From 103ef9678fe3f31098e9793926a5831d54249941 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 6 Dec 2024 16:34:35 +0100 Subject: [PATCH 01/93] Elide capabilities implied by Capability subtypes when printing When printing a type `C^` where `C` extends `Capability`, don't show the `^`. This is overridden under -Yprint-debug. --- .../src/dotty/tools/dotc/printing/PlainPrinter.scala | 11 ++++++++--- tests/neg-custom-args/captures/byname.check | 4 ++-- tests/neg-custom-args/captures/cc-this5.check | 2 +- tests/neg-custom-args/captures/effect-swaps.check | 2 +- .../captures/explain-under-approx.check | 4 ++-- .../captures/extending-cap-classes.check | 6 +++--- tests/neg-custom-args/captures/i21614.check | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index e90aeb217362..bace43b767bd 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -177,11 +177,16 @@ class PlainPrinter(_ctx: Context) extends Printer { * capturing function types. */ protected def toTextCapturing(parent: Type, refsText: Text, boxText: Text): Text = - changePrec(InfixPrec): - boxText ~ toTextLocal(parent) ~ "^" - ~ (refsText provided refsText != rootSetText) + def coreText = boxText ~ toTextLocal(parent) + if parent.derivesFrom(defn.Caps_Capability) + && refsText == impliedByCapabilitySetText + && !printDebug + then coreText + else changePrec(InfixPrec): + coreText~ "^" ~ (refsText provided refsText != rootSetText) final protected def rootSetText = Str("{cap}") // TODO Use disambiguation + final protected def impliedByCapabilitySetText = Str("{cap}") def toText(tp: Type): Text = controlled { homogenize(tp) match { diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 1c113591922d..de2078ddf30a 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -8,10 +8,10 @@ -- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- 19 | h(g()) // error | ^^^ - | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} + | reference (cap2 : Cap) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type () ?->{cap1} I -- Error: tests/neg-custom-args/captures/byname.scala:22:12 ------------------------------------------------------------ 22 | h2(() => g())() // error | ^^^ - | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} + | reference (cap2 : Cap) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type () ->{cap1} I diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index 21b5b36e0574..a69c482300f8 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/cc-this5.scala:16:20 ---------------------------------------------------------- 16 | def f = println(c) // error | ^ - | reference (c : Cap^) is not included in the allowed capture set {} + | reference (c : Cap) is not included in the allowed capture set {} | of the enclosing class A -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:21:15 ------------------------------------- 21 | val x: A = this // error diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index b74c165fd6b6..48dc46c09821 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -25,5 +25,5 @@ -- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:15 ------------------------------------------------------ 66 | Result.make: // error: local reference leaks | ^^^^^^^^^^^ - |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]^): + |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]): | box Future[box T^?]^{fr, contextual$9} leaks into outer capture set of type parameter T of method make in object Result diff --git a/tests/neg-custom-args/captures/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check index c186fc6adb11..f84ac5eb2b53 100644 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ b/tests/neg-custom-args/captures/explain-under-approx.check @@ -1,14 +1,14 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:12:10 ------------------------- 12 | col.add(Future(() => 25)) // error | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async^)}^{async} + | Found: Future[Int]{val a: (async : Async)}^{async} | Required: Future[Int]^{col.futs*} | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 ------------------------- 15 | col1.add(Future(() => 25)) // error | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async^)}^{async} + | Found: Future[Int]{val a: (async : Async)}^{async} | Required: Future[Int]^{col1.futs*} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/extending-cap-classes.check b/tests/neg-custom-args/captures/extending-cap-classes.check index 0936f48576e5..4a77a638a4d8 100644 --- a/tests/neg-custom-args/captures/extending-cap-classes.check +++ b/tests/neg-custom-args/captures/extending-cap-classes.check @@ -1,21 +1,21 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:7:15 ------------------------- 7 | val x2: C1 = new C2 // error | ^^^^^^ - | Found: C2^ + | Found: C2 | Required: C1 | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:8:15 ------------------------- 8 | val x3: C1 = new C3 // error | ^^^^^^ - | Found: C3^ + | Found: C3 | Required: C1 | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:13:15 ------------------------ 13 | val z2: C1 = y2 // error | ^^ - | Found: (y2 : C2^) + | Found: (y2 : C2) | Required: C1 | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index f4967253455f..d4d64424e297 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -1,8 +1,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:33 --------------------------------------- 12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? | ^ - | Found: (f : F^) - | Required: File^ + | Found: (f : F) + | Required: File | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:15:12 --------------------------------------- From 748f4a62aa530eba6c2edf94b7917d1447caf711 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 10 Jan 2025 19:05:44 +0100 Subject: [PATCH 02/93] Add Mutable classes and ReadOnly capabilities - Add Mutable trait and mut modifier. - Add dedicated tests `isMutableVar` and `isMutableVarOrAccessor` so that update methods can share the same flag `Mutable` with mutable vars. - Disallow update methods overriding normal methods - Disallow update methods which are not members of classes extending Mutable - Add design document from papers repo to docs/internals - Add readOnly capabilities - Implement raeadOnly access - Check that update methods are only called on references with exclusive capture sets. - Use cap.rd as default capture set of Capability subtypes - Make Mutable a Capability, this means Mutable class references get {cap.rd} as default capture set. - Use {cap} as captu --- .../tools/backend/jvm/BTypesFromSymbols.scala | 2 +- .../src/dotty/tools/dotc/ast/Desugar.scala | 2 + .../src/dotty/tools/dotc/ast/TreeInfo.scala | 2 +- compiler/src/dotty/tools/dotc/ast/untpd.scala | 3 + .../src/dotty/tools/dotc/cc/CaptureOps.scala | 165 ++++-- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 77 ++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 55 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 81 ++- .../src/dotty/tools/dotc/cc/Existential.scala | 3 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 +- .../dotty/tools/dotc/core/Definitions.scala | 9 +- .../src/dotty/tools/dotc/core/Flags.scala | 1 - .../src/dotty/tools/dotc/core/StdNames.scala | 3 + .../tools/dotc/core/SymDenotations.scala | 7 + .../src/dotty/tools/dotc/core/SymUtils.scala | 2 +- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 44 +- .../dotty/tools/dotc/parsing/Scanners.scala | 5 +- .../tools/dotc/printing/PlainPrinter.scala | 35 +- .../tools/dotc/printing/RefinedPrinter.scala | 4 +- .../dotty/tools/dotc/reporting/messages.scala | 2 +- .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 2 +- .../tools/dotc/transform/CapturedVars.scala | 2 +- .../tools/dotc/transform/CheckReentrant.scala | 2 +- .../tools/dotc/transform/CheckStatic.scala | 2 +- .../tools/dotc/transform/Constructors.scala | 2 +- .../dotty/tools/dotc/transform/LazyVals.scala | 4 +- .../tools/dotc/transform/MoveStatics.scala | 2 +- .../dotc/transform/UninitializedDefs.scala | 2 +- .../tools/dotc/transform/init/Objects.scala | 2 +- .../tools/dotc/transform/init/Util.scala | 2 +- .../src/dotty/tools/dotc/typer/Checking.scala | 10 +- .../tools/dotc/typer/ErrorReporting.scala | 2 +- .../dotty/tools/dotc/typer/Nullables.scala | 6 +- .../tools/dotc/typer/QuotesAndSplices.scala | 2 +- .../dotty/tools/dotc/typer/RefChecks.scala | 9 +- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../tools/dotc/typer/VarianceChecker.scala | 2 +- .../_docs/internals/exclusive-capabilities.md | 551 ++++++++++++++++++ .../internal/readOnlyCapability.scala | 7 + library/src/scala/caps.scala | 15 +- tests/neg-custom-args/captures/i21614.check | 19 +- .../captures/lazylists-exceptions.check | 2 +- .../captures/mut-outside-mutable.check | 8 + .../captures/mut-outside-mutable.scala | 10 + .../captures/mut-override.scala | 19 + tests/neg-custom-args/captures/readOnly.check | 19 + tests/neg-custom-args/captures/readOnly.scala | 22 + tests/neg-custom-args/captures/real-try.check | 10 +- tests/pos-custom-args/captures/mutRef.scala | 5 + tests/pos-custom-args/captures/readOnly.scala | 46 ++ 51 files changed, 1089 insertions(+), 207 deletions(-) create mode 100644 docs/_docs/internals/exclusive-capabilities.md create mode 100644 library/src/scala/annotation/internal/readOnlyCapability.scala create mode 100644 tests/neg-custom-args/captures/mut-outside-mutable.check create mode 100644 tests/neg-custom-args/captures/mut-outside-mutable.scala create mode 100644 tests/neg-custom-args/captures/mut-override.scala create mode 100644 tests/neg-custom-args/captures/readOnly.check create mode 100644 tests/neg-custom-args/captures/readOnly.scala create mode 100644 tests/pos-custom-args/captures/mutRef.scala create mode 100644 tests/pos-custom-args/captures/readOnly.scala diff --git a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala index 97934935f352..817d0be54d26 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala @@ -285,7 +285,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce // tests/run/serialize.scala and https://github.com/typelevel/cats-effect/pull/2360). val privateFlag = !sym.isClass && (sym.is(Private) || (sym.isPrimaryConstructor && sym.owner.isTopLevelModuleClass)) - val finalFlag = sym.is(Final) && !toDenot(sym).isClassConstructor && !sym.is(Mutable, butNot = Accessor) && !sym.enclosingClass.is(Trait) + val finalFlag = sym.is(Final) && !toDenot(sym).isClassConstructor && !sym.isMutableVar && !sym.enclosingClass.is(Trait) import asm.Opcodes.* import GenBCodeOps.addFlagIf diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index c235143e97f1..471a9953c4f0 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -2262,6 +2262,8 @@ object desugar { New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil)) else if op.name == nme.CC_REACH then Apply(ref(defn.Caps_reachCapability), t :: Nil) + else if op.name == nme.CC_READONLY then + Apply(ref(defn.Caps_readOnlyCapability), t :: Nil) else assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode) Select(t, op.name) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 32ab8378ae16..45e17794ec96 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -759,7 +759,7 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => */ def isVariableOrGetter(tree: Tree)(using Context): Boolean = { def sym = tree.symbol - def isVar = sym.is(Mutable) + def isVar = sym.isMutableVarOrAccessor def isGetter = mayBeVarGetter(sym) && sym.owner.info.member(sym.name.asTermName.setterName).exists diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 2acfc4cf86e3..e89dc2c1cdb5 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -206,6 +206,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Var()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable) + case class Mut()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable) + case class Implicit()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Implicit) case class Given()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Given) @@ -332,6 +334,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def isEnumCase: Boolean = isEnum && is(Case) def isEnumClass: Boolean = isEnum && !is(Case) + def isMutableVar: Boolean = is(Mutable) && mods.exists(_.isInstanceOf[Mod.Var]) } @sharable val EmptyModifiers: Modifiers = Modifiers() diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 92cd40a65d5a..1a9421aea142 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -136,6 +136,8 @@ extension (tree: Tree) def toCaptureRefs(using Context): List[CaptureRef] = tree match case ReachCapabilityApply(arg) => arg.toCaptureRefs.map(_.reach) + case ReadOnlyCapabilityApply(arg) => + arg.toCaptureRefs.map(_.readOnly) case CapsOfApply(arg) => arg.toCaptureRefs case _ => tree.tpe.dealiasKeepAnnots match @@ -184,7 +186,7 @@ extension (tp: Type) case tp: TermRef => ((tp.prefix eq NoPrefix) || tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef - || tp.isRootCapability + || tp.isCap ) && !tp.symbol.isOneOf(UnstableValueFlags) case tp: TypeRef => tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet) @@ -193,6 +195,7 @@ extension (tp: Type) case AnnotatedType(parent, annot) => (annot.symbol == defn.ReachCapabilityAnnot || annot.symbol == defn.MaybeCapabilityAnnot + || annot.symbol == defn.ReadOnlyCapabilityAnnot ) && parent.isTrackableRef case _ => false @@ -222,6 +225,8 @@ extension (tp: Type) else tp match case tp @ ReachCapability(_) => tp.singletonCaptureSet + case ReadOnlyCapability(ref) => + ref.deepCaptureSet(includeTypevars) case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet case _ => @@ -345,7 +350,8 @@ extension (tp: Type) def forceBoxStatus(boxed: Boolean)(using Context): Type = tp.widenDealias match case tp @ CapturingType(parent, refs) if tp.isBoxed != boxed => val refs1 = tp match - case ref: CaptureRef if ref.isTracked || ref.isReach => ref.singletonCaptureSet + case ref: CaptureRef if ref.isTracked || ref.isReach || ref.isReadOnly => + ref.singletonCaptureSet case _ => refs CapturingType(parent, refs1, boxed) case _ => @@ -379,23 +385,32 @@ extension (tp: Type) case _ => false + /** Is this a type extending `Mutable` that has update methods? */ + def isMutableType(using Context): Boolean = + tp.derivesFrom(defn.Caps_Mutable) + && tp.membersBasedOnFlags(Mutable | Method, EmptyFlags) + .exists(_.hasAltWith(_.symbol.isUpdateMethod)) + /** Tests whether the type derives from `caps.Capability`, which means * references of this type are maximal capabilities. */ - def derivesFromCapability(using Context): Boolean = tp.dealias match + def derivesFromCapTrait(cls: ClassSymbol)(using Context): Boolean = tp.dealias match case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol - if sym.isClass then sym.derivesFrom(defn.Caps_Capability) - else tp.superType.derivesFromCapability + if sym.isClass then sym.derivesFrom(cls) + else tp.superType.derivesFromCapTrait(cls) case tp: (TypeProxy & ValueType) => - tp.superType.derivesFromCapability + tp.superType.derivesFromCapTrait(cls) case tp: AndType => - tp.tp1.derivesFromCapability || tp.tp2.derivesFromCapability + tp.tp1.derivesFromCapTrait(cls) || tp.tp2.derivesFromCapTrait(cls) case tp: OrType => - tp.tp1.derivesFromCapability && tp.tp2.derivesFromCapability + tp.tp1.derivesFromCapTrait(cls) && tp.tp2.derivesFromCapTrait(cls) case _ => false + def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability) + def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable) + /** Drop @retains annotations everywhere */ def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling val tm = new TypeMap: @@ -406,17 +421,6 @@ extension (tp: Type) mapOver(t) tm(tp) - /** If `x` is a capture ref, its reach capability `x*`, represented internally - * as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`". - * We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x` - * is the union of all capture sets that appear in covariant position in the - * type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}` - * are unrelated. - */ - def reach(using Context): CaptureRef = tp match - case tp: CaptureRef if tp.isTrackableRef => - if tp.isReach then tp else ReachCapability(tp) - /** If `x` is a capture ref, its maybe capability `x?`, represented internally * as `x @maybeCapability`. `x?` stands for a capability `x` that might or might * not be part of a capture set. We have `{} <: {x?} <: {x}`. Maybe capabilities @@ -436,42 +440,43 @@ extension (tp: Type) * but it has fewer issues with type inference. */ def maybe(using Context): CaptureRef = tp match - case tp: CaptureRef if tp.isTrackableRef => - if tp.isMaybe then tp else MaybeCapability(tp) + case tp @ AnnotatedType(_, annot) if annot.symbol == defn.MaybeCapabilityAnnot => tp + case _ => MaybeCapability(tp) - /** If `ref` is a trackable capture ref, and `tp` has only covariant occurrences of a - * universal capture set, replace all these occurrences by `{ref*}`. This implements - * the new aspect of the (Var) rule, which can now be stated as follows: - * - * x: T in E - * ----------- - * E |- x: T' - * - * where T' is T with (1) the toplevel capture set replaced by `{x}` and - * (2) all covariant occurrences of cap replaced by `x*`, provided there - * are no occurrences in `T` at other variances. (1) is standard, whereas - * (2) is new. - * - * For (2), multiple-flipped covariant occurrences of cap won't be replaced. - * In other words, - * - * - For xs: List[File^] ==> List[File^{xs*}], the cap is replaced; - * - while f: [R] -> (op: File^ => R) -> R remains unchanged. - * - * Without this restriction, the signature of functions like withFile: - * - * (path: String) -> [R] -> (op: File^ => R) -> R - * - * could be refined to - * - * (path: String) -> [R] -> (op: File^{withFile*} => R) -> R - * - * which is clearly unsound. - * - * Why is this sound? Covariant occurrences of cap must represent capabilities - * that are reachable from `x`, so they are included in the meaning of `{x*}`. - * At the same time, encapsulation is still maintained since no covariant - * occurrences of cap are allowed in instance types of type variables. + /** If `x` is a capture ref, its reach capability `x*`, represented internally + * as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`". + * We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x` + * is the union of all capture sets that appear in covariant position in the + * type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}` + * are unrelated. + */ + def reach(using Context): CaptureRef = tp match + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.MaybeCapabilityAnnot => + tp.derivedAnnotatedType(tp1.reach, annot) + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReachCapabilityAnnot => + tp + case _ => + ReachCapability(tp) + + /** If `x` is a capture ref, its read-only capability `x.rd`, represented internally + * as `x @readOnlyCapability`. We have {x.rd} <: {x}. If `x` is a reach capability `y*`, + * then its read-only version is `x.rd*`. + */ + def readOnly(using Context): CaptureRef = tp match + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.MaybeCapabilityAnnot + || annot.symbol == defn.ReachCapabilityAnnot => + tp.derivedAnnotatedType(tp1.readOnly, annot) + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReadOnlyCapabilityAnnot => + tp + case _ => + ReadOnlyCapability(tp) + + /** If `x` is a capture ref, replacxe all no-flip covariant occurrences of `cap` + * in type `tp` with `x*`. */ def withReachCaptures(ref: Type)(using Context): Type = object narrowCaps extends TypeMap: @@ -479,9 +484,10 @@ extension (tp: Type) def apply(t: Type) = if variance <= 0 then t else t.dealiasKeepAnnots match - case t @ CapturingType(p, cs) if cs.isUniversal => + case t @ CapturingType(p, cs) if cs.containsRootCapability => change = true - t.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet) + val reachRef = if cs.isReadOnly then ref.reach.readOnly else ref.reach + t.derivedCapturingType(apply(p), reachRef.singletonCaptureSet) case t @ AnnotatedType(parent, ann) => // Don't map annotations, which includes capture sets t.derivedAnnotatedType(this(parent), ann) @@ -615,6 +621,16 @@ extension (sym: Symbol) case c: TypeRef => c.symbol == sym case _ => false + def isUpdateMethod(using Context): Boolean = + sym.isAllOf(Mutable | Method, butNot = Accessor) + + def isReadOnlyMethod(using Context): Boolean = + sym.is(Method, butNot = Mutable | Accessor) && sym.owner.derivesFrom(defn.Caps_Mutable) + + def isInReadOnlyMethod(using Context): Boolean = + if sym.is(Method) && sym.owner.isClass then isReadOnlyMethod + else sym.owner.isInReadOnlyMethod + extension (tp: AnnotatedType) /** Is this a boxed capturing type? */ def isBoxed(using Context): Boolean = tp.annot match @@ -650,6 +666,14 @@ object ReachCapabilityApply: case Apply(reach, arg :: Nil) if reach.symbol == defn.Caps_reachCapability => Some(arg) case _ => None +/** An extractor for `caps.readOnlyCapability(ref)`, which is used to express a read-only + * capability as a tree in a @retains annotation. + */ +object ReadOnlyCapabilityApply: + def unapply(tree: Apply)(using Context): Option[Tree] = tree match + case Apply(ro, arg :: Nil) if ro.symbol == defn.Caps_readOnlyCapability => Some(arg) + case _ => None + /** An extractor for `caps.capsOf[X]`, which is used to express a generic capture set * as a tree in a @retains annotation. */ @@ -658,22 +682,35 @@ object CapsOfApply: case TypeApply(capsOf, arg :: Nil) if capsOf.symbol == defn.Caps_capsOf => Some(arg) case _ => None -class AnnotatedCapability(annot: Context ?=> ClassSymbol): - def apply(tp: Type)(using Context) = +abstract class AnnotatedCapability(annot: Context ?=> ClassSymbol): + def apply(tp: Type)(using Context): AnnotatedType = + assert(tp.isTrackableRef) + tp match + case AnnotatedType(_, annot) => assert(!unwrappable.contains(annot.symbol)) + case _ => AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match case AnnotatedType(parent: CaptureRef, ann) if ann.symbol == annot => Some(parent) case _ => None - -/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express - * the reach capability `ref*` as a type. - */ -object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot) + protected def unwrappable(using Context): Set[Symbol] /** An extractor for `ref @maybeCapability`, which is used to express * the maybe capability `ref?` as a type. */ -object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot) +object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot): + protected def unwrappable(using Context) = Set() + +/** An extractor for `ref @readOnlyCapability`, which is used to express + * the rad-only capability `ref.rd` as a type. + */ +object ReadOnlyCapability extends AnnotatedCapability(defn.ReadOnlyCapabilityAnnot): + protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot) + +/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express + * the reach capability `ref*` as a type. + */ +object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot): + protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot, defn.ReadOnlyCapabilityAnnot) /** Offers utility method to be used for type maps that follow aliases */ trait ConservativeFollowAliasMap(using Context) extends TypeMap: diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 2caba4cf7d89..3a07d88a3ffc 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -15,7 +15,9 @@ import compiletime.uninitialized import StdNames.nme /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, - * as well as two kinds of AnnotatedTypes representing reach and maybe capabilities. + * as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities. + * If there are several annotations they come with an orderL + * `*` first, `.rd` next, `?` last. */ trait CaptureRef extends TypeProxy, ValueType: private var myCaptureSet: CaptureSet | Null = uninitialized @@ -28,39 +30,69 @@ trait CaptureRef extends TypeProxy, ValueType: final def isTracked(using Context): Boolean = this.isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) - /** Is this a reach reference of the form `x*`? */ - final def isReach(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.ReachCapabilityAnnot - case _ => false - /** Is this a maybe reference of the form `x?`? */ - final def isMaybe(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.MaybeCapabilityAnnot - case _ => false + final def isMaybe(using Context): Boolean = this ne stripMaybe - final def stripReach(using Context): CaptureRef = - if isReach then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this + /** Is this a read-only reference of the form `x.rd` or a capture set variable + * with only read-ony references in its upper bound? + */ + final def isReadOnly(using Context): Boolean = this match + case tp: TypeRef => tp.captureSetOfInfo.isReadOnly + case _ => this ne stripReadOnly - final def stripMaybe(using Context): CaptureRef = - if isMaybe then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this + /** Is this a reach reference of the form `x*`? */ + final def isReach(using Context): Boolean = this ne stripReach + + final def stripMaybe(using Context): CaptureRef = this match + case AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.MaybeCapabilityAnnot => + tp1 + case _ => + this + + final def stripReadOnly(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReadOnlyCapabilityAnnot then + tp1 + else if sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReadOnly, annot) + else + this + case _ => + this + + final def stripReach(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReachCapabilityAnnot then + tp1 + else if sym == defn.ReadOnlyCapabilityAnnot || sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReach, annot) + else + this + case _ => + this /** Is this reference the generic root capability `cap` ? */ - final def isRootCapability(using Context): Boolean = this match + final def isCap(using Context): Boolean = this match case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false + /** Is this reference one the generic root capabilities `cap` or `cap.rd` ? */ + final def isRootCapability(using Context): Boolean = this match + case ReadOnlyCapability(tp1) => tp1.isCap + case _ => isCap + /** Is this reference capability that does not derive from another capability ? */ final def isMaxCapability(using Context): Boolean = this match - case tp: TermRef => tp.isRootCapability || tp.info.derivesFrom(defn.Caps_Exists) + case tp: TermRef => tp.isCap || tp.info.derivesFrom(defn.Caps_Exists) case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false + final def isExclusive(using Context): Boolean = + !isReadOnly && (isMaxCapability || captureSetOfInfo.isExclusive) + // With the support of pathes, we don't need to normalize the `TermRef`s anymore. // /** Normalize reference so that it can be compared with `eq` for equality */ // final def normalizedRef(using Context): CaptureRef = this match @@ -130,7 +162,7 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false (this eq y) - || this.isRootCapability + || this.isCap || y.match case y: TermRef if !y.isRootCapability => y.prefix.match @@ -150,6 +182,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 39c41c369864..dc6c391b4ca1 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -83,11 +83,21 @@ sealed abstract class CaptureSet extends Showable: /** Does this capture set contain the root reference `cap` as element? */ final def isUniversal(using Context) = + elems.exists(_.isCap) + + /** Does this capture set contain a root reference `cap` or `cap.rd` as element? */ + final def containsRootCapability(using Context) = elems.exists(_.isRootCapability) final def isUnboxable(using Context) = elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) + final def isReadOnly(using Context): Boolean = + elems.forall(_.isReadOnly) + + final def isExclusive(using Context): Boolean = + elems.exists(_.isExclusive) + final def keepAlways: Boolean = this.isInstanceOf[EmptyWithProvenance] /** Try to include an element in this capture set. @@ -310,6 +320,8 @@ sealed abstract class CaptureSet extends Showable: def maybe(using Context): CaptureSet = map(MaybeMap()) + def readOnly(using Context): CaptureSet = map(ReadOnlyMap()) + /** Invoke handler if this set has (or later aquires) the root capability `cap` */ def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = if isUnboxable then handler() @@ -373,6 +385,10 @@ object CaptureSet: def universal(using Context): CaptureSet = defn.captureRoot.termRef.singletonCaptureSet + /** The shared capture set `{cap.rd}` */ + def shared(using Context): CaptureSet = + defn.captureRoot.termRef.readOnly.singletonCaptureSet + /** Used as a recursion brake */ @sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty) @@ -526,6 +542,8 @@ object CaptureSet: elem.cls.ccLevel.nextInner <= level case ReachCapability(elem1) => levelOK(elem1) + case ReadOnlyCapability(elem1) => + levelOK(elem1) case MaybeCapability(elem1) => levelOK(elem1) case _ => @@ -558,8 +576,10 @@ object CaptureSet: final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = if isConst then this - else if elems.exists(_.isRootCapability) || computingApprox then + else if isUniversal || computingApprox then universal + else if containsRootCapability && isReadOnly then + shared else computingApprox = true try @@ -1026,25 +1046,29 @@ object CaptureSet: /** The current VarState, as passed by the implicit context */ def varState(using state: VarState): VarState = state - /** Maps `x` to `x?` */ - private class MaybeMap(using Context) extends BiTypeMap: + /** A template for maps on capabilities where f(c) <: c and f(f(c)) = c */ + private abstract class NarrowingCapabilityMap(using Context) extends BiTypeMap: + def mapRef(ref: CaptureRef): CaptureRef def apply(t: Type) = t match - case t: CaptureRef if t.isTrackableRef => t.maybe + case t: CaptureRef if t.isTrackableRef => mapRef(t) case _ => mapOver(t) - override def toString = "Maybe" - lazy val inverse = new BiTypeMap: + def apply(t: Type) = t // since f(c) <: c, this is the best inverse + def inverse = NarrowingCapabilityMap.this + override def toString = NarrowingCapabilityMap.this.toString ++ ".inverse" + end NarrowingCapabilityMap - def apply(t: Type) = t match - case t: CaptureRef if t.isMaybe => t.stripMaybe - case t => mapOver(t) - - def inverse = MaybeMap.this + /** Maps `x` to `x?` */ + private class MaybeMap(using Context) extends NarrowingCapabilityMap: + def mapRef(ref: CaptureRef): CaptureRef = ref.maybe + override def toString = "Maybe" - override def toString = "Maybe.inverse" - end MaybeMap + /** Maps `x` to `x.rd` */ + private class ReadOnlyMap(using Context) extends NarrowingCapabilityMap: + def mapRef(ref: CaptureRef): CaptureRef = ref.readOnly + override def toString = "ReadOnly" /* Not needed: def ofClass(cinfo: ClassInfo, argTypes: List[Type])(using Context): CaptureSet = @@ -1073,6 +1097,8 @@ object CaptureSet: case ReachCapability(ref1) => ref1.widen.deepCaptureSet(includeTypevars = true) .showing(i"Deep capture set of $ref: ${ref1.widen} = ${result}", capt) + case ReadOnlyCapability(ref1) => + ref1.captureSetOfInfo.map(ReadOnlyMap()) case _ => if ref.isMaxCapability then ref.singletonCaptureSet else ofType(ref.underlying, followResult = true) @@ -1196,9 +1222,10 @@ object CaptureSet: for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield ccState.levelError = None if ref.isRootCapability then + def capStr = if ref.isReadOnly then "cap.rd" else "cap" i""" | - |Note that the universal capability `cap` + |Note that the universal capability `$capStr` |cannot be included in capture set $cs""" else val levelStr = ref match diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 830d9ad0a4d4..eab11d03144d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -150,6 +150,7 @@ object CheckCaptures: |is must be a type parameter or abstract type with a caps.CapSet upper bound.""", elem.srcPos) case ReachCapabilityApply(arg) => check(arg, elem.srcPos) + case ReadOnlyCapabilityApply(arg) => check(arg, elem.srcPos) case _ => check(elem, elem.srcPos) /** Under the sealed policy, report an error if some part of `tp` contains the @@ -381,7 +382,7 @@ class CheckCaptures extends Recheck, SymTransformer: def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = markFree(sym, sym.termRef, pos) - def markFree(sym: Symbol, ref: TermRef, pos: SrcPos)(using Context): Unit = + def markFree(sym: Symbol, ref: CaptureRef, pos: SrcPos)(using Context): Unit = if sym.exists && ref.isTracked then markFree(ref.captureSet, pos) /** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing @@ -484,7 +485,8 @@ class CheckCaptures extends Recheck, SymTransformer: def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => - if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) + if sym.exists then + if curEnv.isOpen then markFree(capturedVars(sym), pos) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType @@ -530,13 +532,18 @@ class CheckCaptures extends Recheck, SymTransformer: // expected type `pt`. // Example: If we have `x` and the expected type says we select that with `.a.b`, // we charge `x.a.b` instead of `x`. - def addSelects(ref: TermRef, pt: Type): TermRef = pt match + def addSelects(ref: TermRef, pt: Type): CaptureRef = pt match case pt: PathSelectionProto if ref.isTracked => - // if `ref` is not tracked then the selection could not give anything new - // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. - addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) + if pt.sym.isReadOnlyMethod then + ref.readOnly + else + // if `ref` is not tracked then the selection could not give anything new + // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. + addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) case _ => ref - val pathRef = addSelects(sym.termRef, pt) + var pathRef: CaptureRef = addSelects(sym.termRef, pt) + if pathRef.derivesFrom(defn.Caps_Mutable) && pt.isValueType && !pt.isMutableType then + pathRef = pathRef.readOnly markFree(sym, pathRef, tree.srcPos) super.recheckIdent(tree, pt) @@ -545,7 +552,9 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def selectionProto(tree: Select, pt: Type)(using Context): Type = val sym = tree.symbol - if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic then PathSelectionProto(sym, pt) + if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic + || sym.isReadOnlyMethod + then PathSelectionProto(sym, pt) else super.selectionProto(tree, pt) /** A specialized implementation of the selection rule. @@ -573,6 +582,12 @@ class CheckCaptures extends Recheck, SymTransformer: } case _ => denot + if tree.symbol.isUpdateMethod && !qualType.captureSet.isExclusive then + report.error( + em"""cannot call update ${tree.symbol} from $qualType, + |since its capture set ${qualType.captureSet} is read-only""", + tree.srcPos) + val selType = recheckSelection(tree, qualType, name, disambiguate) val selWiden = selType.widen @@ -731,7 +746,9 @@ class CheckCaptures extends Recheck, SymTransformer: def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = - if core.derivesFromCapability then defn.universalCSImpliedByCapability else initCs + if core.derivesFromMutable then CaptureSet.universal + else if core.derivesFromCapability then initCs ++ defn.universalCSImpliedByCapability + else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol if !getter.is(Private) && getter.hasTrackedParts then @@ -1105,6 +1122,7 @@ class CheckCaptures extends Recheck, SymTransformer: if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then markFree(res.boxedCaptureSet, tree.srcPos) res + end recheck /** Under the old unsealed policy: check that cap is ot unboxed */ override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = @@ -1427,6 +1445,25 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => widened case _ => widened + /** If actual is a capturing type T^C extending Mutable, and expected is an + * unboxed non-singleton value type not extending mutable, narrow the capture + * set `C` to `ro(C)`. + * The unboxed condition ensures that the expected is not a type variable + * that's upper bounded by a read-only type. In this case it would not be sound + * to narrow to the read-only set, since that set can be propagated + * by the type variable instantiatiin. + */ + private def improveReadOnly(actual: Type, expected: Type)(using Context): Type = actual match + case actual @ CapturingType(parent, refs) + if parent.derivesFrom(defn.Caps_Mutable) + && expected.isValueType + && !expected.isMutableType + && !expected.isSingleton + && !expected.isBoxedCapturing => + actual.derivedCapturingType(parent, refs.readOnly) + case _ => + actual + /** Adapt `actual` type to `expected` type. This involves: * - narrow toplevel captures of `x`'s underlying type to `{x}` according to CC's VAR rule * - narrow nested captures of `x`'s underlying type to `{x*}` @@ -1436,12 +1473,14 @@ class CheckCaptures extends Recheck, SymTransformer: if expected == LhsProto || expected.isSingleton && actual.isSingleton then actual else - val widened = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improvedRO = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( - widened.withReachCaptures(actual), expected, pos, + improvedRO.withReachCaptures(actual), expected, pos, covariant = true, alwaysConst = false, boxErrors) - if adapted eq widened then actual - else adapted.showing(i"adapt boxed $actual vs $expected = $adapted", capt) + if adapted eq improvedVAR // no .rd improvement, no box-adaptation + then actual // might as well use actual instead of improved widened + else adapted.showing(i"adapt $actual vs $expected = $adapted", capt) end adapt // ---- Unit-level rechecking ------------------------------------------- @@ -1484,18 +1523,16 @@ class CheckCaptures extends Recheck, SymTransformer: /** Check that overrides don't change the @use status of their parameters */ override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit = + def fail(msg: String) = + report.error( + OverrideError(msg, self, member, other, self.memberInfo(member), self.memberInfo(other)), + if member.owner == clazz then member.srcPos else clazz.srcPos) for (params1, params2) <- member.rawParamss.lazyZip(other.rawParamss) (param1, param2) <- params1.lazyZip(params2) do if param1.hasAnnotation(defn.UseAnnot) != param2.hasAnnotation(defn.UseAnnot) then - report.error( - OverrideError( - i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition", - self, member, other, self.memberInfo(member), self.memberInfo(other) - ), - if member.owner == clazz then member.srcPos else clazz.srcPos - ) + fail(i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition") end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = @@ -1526,7 +1563,7 @@ class CheckCaptures extends Recheck, SymTransformer: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then capturedBy(sym) = enclMeth @@ -1601,7 +1638,7 @@ class CheckCaptures extends Recheck, SymTransformer: selfType match case CapturingType(_, refs: CaptureSet.Var) if !root.isEffectivelySealed - && !refs.elems.exists(_.isRootCapability) + && !refs.isUniversal && !root.matchesExplicitRefsInBaseClass(refs) => // Forbid inferred self types unless they are already implied by an explicit diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index ea979e0b9f7f..943254a7ba4e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -4,7 +4,6 @@ package cc import core.* import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* -import CaptureSet.IdempotentCaptRefMap import StdNames.nme import ast.tpd.* import Decorators.* @@ -303,7 +302,7 @@ object Existential: class Wrap(boundVar: TermParamRef) extends CapMap: def apply(t: Type) = t match - case t: TermRef if t.isRootCapability => + case t: TermRef if t.isCap => if variance > 0 then needsWrap = true boundVar diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index a5e96f1f9ce2..8a353778c44e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -443,7 +443,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: try transformTT(tpt, boxed = - sym.is(Mutable, butNot = Method) + sym.isMutableVar && !ccConfig.useSealed && !sym.hasAnnotation(defn.UncheckedCapturesAnnot), // Under the sealed policy, we disallow root capabilities in the type of mutable @@ -735,7 +735,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case RetainingType(parent, refs) => needsVariable(parent) && !refs.tpes.exists: - case ref: TermRef => ref.isRootCapability + case ref: TermRef => ref.isCap case _ => false case AnnotatedType(parent, _) => needsVariable(parent) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f89bc8691e2d..ac9f42a6fad0 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 @@ -998,18 +998,20 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredModule("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") - @tu lazy val Caps_Capability: TypeSymbol = CapsModule.requiredType("Capability") + @tu lazy val Caps_Capability: ClassSymbol = requiredClass("scala.caps.Capability") @tu lazy val Caps_CapSet: ClassSymbol = requiredClass("scala.caps.CapSet") @tu lazy val Caps_reachCapability: TermSymbol = CapsModule.requiredMethod("reachCapability") + @tu lazy val Caps_readOnlyCapability: TermSymbol = CapsModule.requiredMethod("readOnlyCapability") @tu lazy val Caps_capsOf: TermSymbol = CapsModule.requiredMethod("capsOf") @tu lazy val Caps_Exists: ClassSymbol = requiredClass("scala.caps.Exists") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_containsImpl: TermSymbol = CapsModule.requiredMethod("containsImpl") + @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") /** The same as CaptureSet.universal but generated implicitly for references of Capability subtypes */ - @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef) + @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef.readOnly) @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") @@ -1083,6 +1085,7 @@ class Definitions { @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val ReachCapabilityAnnot = requiredClass("scala.annotation.internal.reachCapability") + @tu lazy val ReadOnlyCapabilityAnnot = requiredClass("scala.annotation.internal.readOnlyCapability") @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @tu lazy val RetainsCapAnnot: ClassSymbol = requiredClass("scala.annotation.retainsCap") diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 0775b3caaf0c..57bf870c6b64 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -597,7 +597,6 @@ object Flags { val JavaInterface: FlagSet = JavaDefined | NoInits | Trait val JavaProtected: FlagSet = JavaDefined | Protected val MethodOrLazy: FlagSet = Lazy | Method - val MutableOrLazy: FlagSet = Lazy | Mutable val MethodOrLazyOrMutable: FlagSet = Lazy | Method | Mutable val LiftedMethod: FlagSet = Lifted | Method val LocalParam: FlagSet = Local | Param diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 90e5544f19af..c33c795571e6 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -121,6 +121,7 @@ object StdNames { val BITMAP_CHECKINIT: N = s"${BITMAP_PREFIX}init$$" // initialization bitmap for checkinit values val BITMAP_CHECKINIT_TRANSIENT: N = s"${BITMAP_PREFIX}inittrans$$" // initialization bitmap for transient checkinit values val CC_REACH: N = "$reach" + val CC_READONLY: N = "$readOnly" val DEFAULT_GETTER: N = str.DEFAULT_GETTER val DEFAULT_GETTER_INIT: N = "$lessinit$greater" val DO_WHILE_PREFIX: N = "doWhile$" @@ -554,6 +555,7 @@ object StdNames { val materializeTypeTag: N = "materializeTypeTag" val mirror : N = "mirror" val moduleClass : N = "moduleClass" + val mut: N = "mut" val name: N = "name" val nameDollar: N = "$name" val ne: N = "ne" @@ -588,6 +590,7 @@ object StdNames { val productPrefix: N = "productPrefix" val quotes : N = "quotes" val raw_ : N = "raw" + val rd: N = "rd" val refl: N = "refl" val reflect: N = "reflect" val reflectiveSelectable: N = "reflectiveSelectable" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 54e18bf1ea1b..e7eda037117c 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -806,6 +806,13 @@ object SymDenotations { final def isRealMethod(using Context): Boolean = this.is(Method, butNot = Accessor) && !isAnonymousFunction + /** A mutable variable (not a getter or setter for it) */ + final def isMutableVar(using Context): Boolean = is(Mutable, butNot = Method) + + /** A mutable variable or its getter or setter */ + final def isMutableVarOrAccessor(using Context): Boolean = + is(Mutable) && (!is(Method) || is(Accessor)) + /** Is this a getter? */ final def isGetter(using Context): Boolean = this.is(Accessor) && !originalName.isSetterName && !(originalName.isScala2LocalSuffix && symbol.owner.is(Scala2x)) diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 54ba0e3bdd06..1b83014e5735 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -287,7 +287,7 @@ class SymUtils: */ def isConstExprFinalVal(using Context): Boolean = atPhaseNoLater(erasurePhase) { - self.is(Final, butNot = Mutable) && self.info.resultType.isInstanceOf[ConstantType] + self.is(Final) && !self.isMutableVarOrAccessor && self.info.resultType.isInstanceOf[ConstantType] } && !self.sjsNeedsField /** The `ConstantType` of a val known to be `isConstrExprFinalVal`. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index cc0471d40213..73e53138829b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2174,7 +2174,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 20eb6e9b33fa..c61c2703eb1a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1600,22 +1600,36 @@ object Parsers { case _ => None } - /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] + /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [`.` rd] * | [ { SimpleRef `.` } SimpleRef `.` ] id `^` */ def captureRef(): Tree = - val ref = dotSelectors(simpleRef()) - if isIdent(nme.raw.STAR) then - in.nextToken() - atSpan(startOffset(ref)): - PostfixOp(ref, Ident(nme.CC_REACH)) - else if isIdent(nme.UPARROW) then + + def derived(ref: Tree, name: TermName) = in.nextToken() - atSpan(startOffset(ref)): - convertToTypeId(ref) match - case ref: RefTree => makeCapsOf(ref) - case ref => ref - else ref + atSpan(startOffset(ref)) { PostfixOp(ref, Ident(name)) } + + def recur(ref: Tree): Tree = + if in.token == DOT then + in.nextToken() + if in.isIdent(nme.rd) then derived(ref, nme.CC_READONLY) + else recur(selector(ref)) + else if in.isIdent(nme.raw.STAR) then + val reachRef = derived(ref, nme.CC_REACH) + if in.token == DOT && in.lookahead.isIdent(nme.rd) then + in.nextToken() + derived(reachRef, nme.CC_READONLY) + else reachRef + else if isIdent(nme.UPARROW) then + in.nextToken() + atSpan(startOffset(ref)): + convertToTypeId(ref) match + case ref: RefTree => makeCapsOf(ref) + case ref => ref + else ref + + recur(simpleRef()) + end captureRef /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking */ @@ -3304,13 +3318,14 @@ object Parsers { case SEALED => Mod.Sealed() case IDENTIFIER => name match { - case nme.erased if in.erasedEnabled => Mod.Erased() case nme.inline => Mod.Inline() case nme.opaque => Mod.Opaque() case nme.open => Mod.Open() case nme.transparent => Mod.Transparent() case nme.infix => Mod.Infix() case nme.tracked => Mod.Tracked() + case nme.erased if in.erasedEnabled => Mod.Erased() + case nme.mut if Feature.ccEnabled => Mod.Mut() } } @@ -4691,7 +4706,8 @@ object Parsers { syntaxError(msg, tree.span) Nil tree match - case tree: MemberDef if !(tree.mods.flags & (ModifierFlags &~ Mutable)).isEmpty => + case tree: MemberDef + if !(tree.mods.flags & ModifierFlags).isEmpty && !tree.mods.isMutableVar => // vars are OK, mut defs are not fail(em"refinement cannot be ${(tree.mods.flags & ModifierFlags).flagStrings().mkString("`", "`, `", "`")}") case tree: DefDef if tree.termParamss.nestedExists(!_.rhs.isEmpty) => fail(em"refinement cannot have default arguments") diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index ed20c189796b..f7050cec41fd 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1209,7 +1209,10 @@ object Scanners { def isSoftModifier: Boolean = token == IDENTIFIER - && (softModifierNames.contains(name) || name == nme.erased && erasedEnabled || name == nme.tracked && trackedEnabled) + && (softModifierNames.contains(name) + || name == nme.erased && erasedEnabled + || name == nme.tracked && trackedEnabled + || name == nme.mut && Feature.ccEnabled) def isSoftModifierInModifierPosition: Boolean = isSoftModifier && inModifierPosition() diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index bace43b767bd..0f8e81154058 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -167,8 +167,9 @@ class PlainPrinter(_ctx: Context) extends Printer { toTextCaptureRef(ref.typeOpt) case TypeApply(fn, arg :: Nil) if fn.symbol == defn.Caps_capsOf => toTextRetainedElem(arg) - case _ => - toText(ref) + case ReachCapabilityApply(ref1) => toTextRetainedElem(ref1) ~ "*" + case ReadOnlyCapabilityApply(ref1) => toTextRetainedElem(ref1) ~ ".rd" + case _ => toText(ref) private def toTextRetainedElems[T <: Untyped](refs: List[Tree[T]]): Text = "{" ~ Text(refs.map(ref => toTextRetainedElem(ref)), ", ") ~ "}" @@ -177,16 +178,10 @@ class PlainPrinter(_ctx: Context) extends Printer { * capturing function types. */ protected def toTextCapturing(parent: Type, refsText: Text, boxText: Text): Text = - def coreText = boxText ~ toTextLocal(parent) - if parent.derivesFrom(defn.Caps_Capability) - && refsText == impliedByCapabilitySetText - && !printDebug - then coreText - else changePrec(InfixPrec): - coreText~ "^" ~ (refsText provided refsText != rootSetText) + changePrec(InfixPrec): + boxText ~ toTextLocal(parent) ~ "^" ~ (refsText provided refsText != rootSetText) final protected def rootSetText = Str("{cap}") // TODO Use disambiguation - final protected def impliedByCapabilitySetText = Str("{cap}") def toText(tp: Type): Text = controlled { homogenize(tp) match { @@ -195,7 +190,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: TermRef if !tp.denotationIsCurrent && !homogenizedView // always print underlying when testing picklers - && !tp.isRootCapability + && !tp.isCap || tp.symbol.is(Module) || tp.symbol.name == nme.IMPORT => toTextRef(tp) ~ ".type" @@ -247,9 +242,16 @@ class PlainPrinter(_ctx: Context) extends Printer { }.close case tp @ CapturingType(parent, refs) => val boxText: Text = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value - val showAsCap = refs.isUniversal && (refs.elems.size == 1 || !printDebug) - val refsText = if showAsCap then rootSetText else toTextCaptureSet(refs) - toTextCapturing(parent, refsText, boxText) + if parent.derivesFrom(defn.Caps_Capability) + && refs.containsRootCapability && refs.isReadOnly && !printDebug + then + toText(parent) + else + val refsText = + if refs.isUniversal && (refs.elems.size == 1 || !printDebug) + then rootSetText + else toTextCaptureSet(refs) + toTextCapturing(parent, refsText, boxText) case tp @ RetainingType(parent, refs) => if Feature.ccEnabledSomewhere then val refsText = refs match @@ -425,6 +427,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: TermRef if tp.symbol == defn.captureRoot => Str("cap") case tp: SingletonType => toTextRef(tp) case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^" + case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" case tp => toText(tp) @@ -541,7 +544,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if sym.is(Param) then "parameter" else if sym.is(Given) then "given instance" else if (flags.is(Lazy)) "lazy value" - else if (flags.is(Mutable)) "variable" + else if (sym.isMutableVar) "variable" else if (sym.isClassConstructor && sym.isPrimaryConstructor) "primary constructor" else if (sym.isClassConstructor) "constructor" else if (sym.is(Method)) "method" @@ -557,7 +560,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if (flags.is(Module)) "object" else if (sym.isClass) "class" else if (sym.isType) "type" - else if (flags.is(Mutable)) "var" + else if (sym.isMutableVarOrAccessor) "var" else if (flags.is(Package)) "package" else if (sym.is(Method)) "def" else if (sym.isTerm && !flags.is(Param)) "val" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 32115e6bc087..4c1da6ed1e1d 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -337,7 +337,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) @@ -744,6 +744,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case PostfixOp(l, op) => if op.name == nme.CC_REACH then changePrec(DotPrec) { toText(l) ~ "*" } + else if op.name == nme.CC_READONLY then + changePrec(DotPrec) { toText(l) ~ ".rd" } else changePrec(InfixPrec) { toText(l) ~ " " ~ toText(op) } case PrefixOp(op, r) => diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index b5d67f0808b2..dcd7ed10987b 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -1694,7 +1694,7 @@ class OnlyClassesCanHaveDeclaredButUndefinedMembers(sym: Symbol)( def msg(using Context) = i"""Declaration of $sym not allowed here: only classes can have declared but undefined members""" def explain(using Context) = - if sym.is(Mutable) then "Note that variables need to be initialized to be defined." + if sym.isMutableVarOrAccessor then "Note that variables need to be initialized to be defined." else "" } diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index c303c40485ce..4d915b57df1b 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -418,7 +418,7 @@ private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol]) apiClass(sym.asClass) } else if (sym.isType) { apiTypeMember(sym.asType) - } else if (sym.is(Mutable, butNot = Accessor)) { + } else if (sym.isMutableVar) { api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym), apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info)) } else if (sym.isStableMember && !sym.isRealMethod) { diff --git a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala index c1725cbd0255..7263bce0478c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala +++ b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala @@ -120,7 +120,7 @@ object CapturedVars: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then report.log(i"capturing $sym in ${sym.enclosingMethod}, referenced from $enclMeth") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala index e8a402068bfc..5f52ac82879a 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala @@ -65,7 +65,7 @@ class CheckReentrant extends MiniPhase { scanning(cls) { for (sym <- cls.classInfo.decls) if (sym.isTerm && !sym.isSetter && !isIgnored(sym)) - if (sym.is(Mutable)) { + if (sym.isMutableVarOrAccessor) { report.error( em"""possible data race involving globally reachable ${sym.showLocated}: ${sym.info} | use -Ylog:checkReentrant+ to find out more about why the variable is reachable.""") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala index 6c74f302b65d..957fd78e9c2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala @@ -52,7 +52,7 @@ class CheckStatic extends MiniPhase { report.error(MissingCompanionForStatic(defn.symbol), defn.srcPos) else if (clashes.exists) report.error(MemberWithSameNameAsStatic(), defn.srcPos) - else if (defn.symbol.is(Flags.Mutable) && companion.is(Flags.Trait)) + else if (defn.symbol.isMutableVarOrAccessor && companion.is(Flags.Trait)) report.error(TraitCompanionWithMutableStatic(), defn.srcPos) else if (defn.symbol.is(Flags.Lazy)) report.error(LazyStaticField(), defn.srcPos) diff --git a/compiler/src/dotty/tools/dotc/transform/Constructors.scala b/compiler/src/dotty/tools/dotc/transform/Constructors.scala index 9a0df830c6d7..b373565489f0 100644 --- a/compiler/src/dotty/tools/dotc/transform/Constructors.scala +++ b/compiler/src/dotty/tools/dotc/transform/Constructors.scala @@ -155,7 +155,7 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase = case Ident(_) | Select(This(_), _) => var sym = tree.symbol def isOverridableSelect = tree.isInstanceOf[Select] && !sym.isEffectivelyFinal - def switchOutsideSupercall = !sym.is(Mutable) && !isOverridableSelect + def switchOutsideSupercall = !sym.isMutableVarOrAccessor && !isOverridableSelect // If true, switch to constructor parameters also in the constructor body // that follows the super call. // Variables need to go through the getter since they might have been updated. diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index e2712a7d6302..2fd777f715d9 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -255,7 +255,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { def transformMemberDefThreadUnsafe(x: ValOrDefDef)(using Context): Thicket = { val claz = x.symbol.owner.asClass val tpe = x.tpe.widen.resultType.widen - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) val containerName = LazyLocalName.fresh(x.name.asTermName) val containerSymbol = newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags | Private, @@ -447,7 +447,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) if ctx.settings.YlegacyLazyVals.value then transformMemberDefThreadSafeLegacy(x) else diff --git a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala index 95975ad9e6b8..b3ec05501b5b 100644 --- a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala +++ b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala @@ -28,7 +28,7 @@ class MoveStatics extends MiniPhase with SymTransformer { def transformSym(sym: SymDenotation)(using Context): SymDenotation = if (sym.hasAnnotation(defn.ScalaStaticAnnot) && sym.owner.is(Flags.Module) && sym.owner.companionClass.exists && - (sym.is(Flags.Method) || !(sym.is(Flags.Mutable) && sym.owner.companionClass.is(Flags.Trait)))) { + (sym.is(Flags.Method) || !(sym.isMutableVarOrAccessor && sym.owner.companionClass.is(Flags.Trait)))) { sym.owner.asClass.delete(sym.symbol) sym.owner.companionClass.asClass.enter(sym.symbol) sym.copySymDenotation(owner = sym.owner.companionClass) diff --git a/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala b/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala index f22fc53e9b6e..7531b6e41c19 100644 --- a/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala +++ b/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala @@ -33,7 +33,7 @@ class UninitializedDefs extends MiniPhase: def recur(rhs: Tree): Boolean = rhs match case rhs: RefTree => rhs.symbol == defn.Compiletime_uninitialized - && tree.symbol.is(Mutable) && tree.symbol.owner.isClass + && tree.symbol.isMutableVarOrAccessor && tree.symbol.owner.isClass case closureDef(ddef) if defn.isContextFunctionType(tree.tpt.tpe.dealias) => recur(ddef.rhs) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 328446a02e23..226e8dbf3fb4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -1032,7 +1032,7 @@ class Objects(using Context @constructorOnly): UnknownValue else if target.exists then def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next" - if target.isOneOf(Flags.Mutable) && !isNextFieldOfColonColon then + if target.isMutableVarOrAccessor && !isNextFieldOfColonColon then if ref.hasVar(target) then val addr = ref.varAddr(target) if addr.owner == State.currentObject then diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index e11d0e1e21a5..ca30e2d32a4d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -112,5 +112,5 @@ object Util: /** Whether the class or its super class/trait contains any mutable fields? */ def isMutable(cls: ClassSymbol)(using Context): Boolean = - cls.classInfo.decls.exists(_.is(Flags.Mutable)) || + cls.classInfo.decls.exists(_.isMutableVarOrAccessor) || cls.parentSyms.exists(parentCls => isMutable(parentCls.asClass)) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index ec07fefc64ab..e044d4f09e32 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -37,7 +37,7 @@ import config.Feature, Feature.{sourceVersion, modularity} import config.SourceVersion.* import config.MigrationVersion import printing.Formatting.hlAsKeyword -import cc.{isCaptureChecking, isRetainsLike} +import cc.{isCaptureChecking, isRetainsLike, isUpdateMethod} import collection.mutable import reporting.* @@ -596,7 +596,7 @@ object Checking { if (sym.isConstructor && !sym.isPrimaryConstructor && sym.owner.is(Trait, butNot = JavaDefined)) val addendum = if ctx.settings.Ydebug.value then s" ${sym.owner.flagsString}" else "" fail(em"Traits cannot have secondary constructors$addendum") - checkApplicable(Inline, sym.isTerm && !sym.isOneOf(Mutable | Module)) + checkApplicable(Inline, sym.isTerm && !sym.is(Module) && !sym.isMutableVarOrAccessor) checkApplicable(Lazy, !sym.isOneOf(Method | Mutable)) if (sym.isType && !sym.isOneOf(Deferred | JavaDefined)) for (cls <- sym.allOverriddenSymbols.filter(_.isClass)) { @@ -605,8 +605,12 @@ object Checking { } if sym.isWrappedToplevelDef && !sym.isType && sym.flags.is(Infix, butNot = Extension) then fail(ModifierNotAllowedForDefinition(Flags.Infix, s"A top-level ${sym.showKind} cannot be infix.")) + if sym.isUpdateMethod && !sym.owner.derivesFrom(defn.Caps_Mutable) then + fail(em"Update methods can only be used as members of classes deriving from the `Mutable` trait") checkApplicable(Erased, - !sym.isOneOf(MutableOrLazy, butNot = Given) && !sym.isType || sym.isClass) + !sym.is(Lazy, butNot = Given) + && !sym.isMutableVarOrAccessor + && (!sym.isType || sym.isClass)) checkCombination(Final, Open) checkCombination(Sealed, Open) checkCombination(Final, Sealed) diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 13e75be75838..58119981dfc4 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -85,7 +85,7 @@ object ErrorReporting { /** An explanatory note to be added to error messages * when there's a problem with abstract var defs */ def abstractVarMessage(sym: Symbol): String = - if (sym.underlyingSymbol.is(Mutable)) + if sym.underlyingSymbol.isMutableVarOrAccessor then "\n(Note that variables need to be initialized to be defined)" else "" diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 310ca999f4c5..86b9a337e69a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -253,7 +253,7 @@ object Nullables: val mutables = infos.foldLeft(Set[TermRef]()): (ms, info) => ms.union( if info.asserted == null then Set.empty - else info.asserted.filter(_.symbol.is(Mutable))) + else info.asserted.filter(_.symbol.isMutableVarOrAccessor)) infos.extendWith(NotNullInfo(Set(), mutables)) end extension @@ -307,7 +307,7 @@ object Nullables: || s.isClass // not in a class || recur(s.owner)) - refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions + refSym.isMutableVarOrAccessor // if it is immutable, we don't need to check the rest conditions && refOwner.isTerm && recur(ctx.owner) end extension @@ -574,7 +574,7 @@ object Nullables: object dropNotNull extends TreeMap: var dropped: Boolean = false override def transform(t: Tree)(using Context) = t match - case AssertNotNull(t0) if t0.symbol.is(Mutable) => + case AssertNotNull(t0) if t0.symbol.isMutableVarOrAccessor => nullables.println(i"dropping $t") dropped = true transform(t0) diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 59993a69797d..4e7c4336b852 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -130,7 +130,7 @@ trait QuotesAndSplices { report.error("Open pattern expected an identifier", arg.srcPos) EmptyTree } - for arg <- typedArgs if arg.symbol.is(Mutable) do // TODO support these patterns. Possibly using scala.quoted.util.Var + for arg <- typedArgs if arg.symbol.isMutableVarOrAccessor do // TODO support these patterns. Possibly using scala.quoted.util.Var report.error("References to `var`s cannot be used in higher-order pattern", arg.srcPos) val argTypes = typedArgs.map(_.tpe.widenTermRefExpr) val patType = (tree.typeargs.isEmpty, tree.args.isEmpty) match diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index a015348e90a7..6c2a7df06ed0 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.* @@ -595,7 +595,7 @@ object RefChecks { overrideError("needs `override` modifier") else if (other.is(AbsOverride) && other.isIncompleteIn(clazz) && !member.is(AbsOverride)) overrideError("needs `abstract override` modifiers") - else if member.is(Override) && other.is(Mutable) then + else if member.is(Override) && other.isMutableVarOrAccessor then overrideError("cannot override a mutable variable") else if (member.isAnyOverride && !(member.owner.thisType.baseClasses exists (_ isSubClass other.owner)) && @@ -616,6 +616,8 @@ object RefChecks { overrideError("is erased, cannot override non-erased member") else if (other.is(Erased) && !member.isOneOf(Erased | Inline)) // (1.9) overrideError("is not erased, cannot override erased member") + else if member.isUpdateMethod && !other.is(Mutable) then + overrideError(i"is an update method, cannot override a read-only method") else if other.is(Inline) && !member.is(Inline) then // (1.10) overrideError("is not inline, cannot implement an inline method") else if (other.isScala2Macro && !member.isScala2Macro) // (1.11) @@ -775,7 +777,7 @@ object RefChecks { // Give a specific error message for abstract vars based on why it fails: // It could be unimplemented, have only one accessor, or be uninitialized. - if (underlying.is(Mutable)) { + if underlying.isMutableVarOrAccessor then val isMultiple = grouped.getOrElse(underlying.name, Nil).size > 1 // If both getter and setter are missing, squelch the setter error. @@ -784,7 +786,6 @@ object RefChecks { if (member.isSetter) "\n(Note that an abstract var requires a setter in addition to the getter)" else if (member.isGetter && !isMultiple) "\n(Note that an abstract var requires a getter in addition to the setter)" else err.abstractVarMessage(member)) - } else if (underlying.is(Method)) { // If there is a concrete method whose name matches the unimplemented // abstract method, and a cursory examination of the difference reveals diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index be3186720fa1..03782a423fc7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1393,7 +1393,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer cpy.Assign(tree)(lhsCore, typed(tree.rhs, lhs1.tpe.widen)).withType(defn.UnitType) def canAssign(sym: Symbol) = - sym.is(Mutable, butNot = Accessor) || + sym.isMutableVar || ctx.owner.isPrimaryConstructor && !sym.is(Method) && sym.maybeOwner == ctx.owner.owner || // allow assignments from the primary constructor to class fields ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor diff --git a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala index 3699ca80d011..0c2929283ee3 100644 --- a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala +++ b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala @@ -157,7 +157,7 @@ class VarianceChecker(using Context) { def isLocal = base.isAllOf(PrivateLocal) || base.is(Private) && !base.hasAnnotation(defn.AssignedNonLocallyAnnot) - if base.is(Mutable, butNot = Method) && !isLocal then + if base.isMutableVar && !isLocal then base.removeAnnotation(defn.AssignedNonLocallyAnnot) variance = 0 try checkInfo(base.info) diff --git a/docs/_docs/internals/exclusive-capabilities.md b/docs/_docs/internals/exclusive-capabilities.md new file mode 100644 index 000000000000..97c6592ac693 --- /dev/null +++ b/docs/_docs/internals/exclusive-capabilities.md @@ -0,0 +1,551 @@ +# Exclusive Capabilities + +Language design draft + + +## Capability Kinds + +A capability is called + - _exclusive_ if it is `cap` or it has an exclusive capability in its capture set. + - _shared_ otherwise. + +There is a new top capability `shared` which can be used as a capability for deriving shared capture sets. Other shared capabilities are created as read-only versions of exclusive capabilities. + +## Update Methods + +We introduce a new trait +```scala +trait Mutable +``` +It is used as a base trait for types that define _update methods_ using +a new modifier `mut`. + +`mut` can only be used in classes or objects extending `Mutable`. An update method is allowed to access exclusive capabilities in the method's environment. By contrast, a normal method in a type extending `Mutable` may access exclusive capabilities only if they are defined locally or passed to it in parameters. + +**Example:** +```scala +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x +``` +Here, `put` needs to be declared as an update method since it accesses the exclusive write capability of the variable `current` in its environment. +`mut` can also be used on an inner class of a class or object extending `Mutable`. It gives all code in the class the right +to access exclusive capabilities in the class environment. Normal classes +can only access exclusive capabilities defined in the class or passed to it in parameters. + +```scala +object Registry extends Mutable: + var count = 0 + mut class Counter: + mut def next: Int = + count += 1 + count +``` +Normal method members of `Mutable` classes cannot call update methods. This is indicated since accesses in the callee are recorded in the caller. So if the callee captures exclusive capabilities so does the caller. + +An update method cannot implement or override a normal method, whereas normal methods may implement or override update methods. Since methods such as `toString` or `==` inherited from Object are normal methods, it follows that none of these methods may be implemented as an update method. + +The `apply` method of a function type is also a normal method, hence `Mutable` classes may not implement a function type with an update method as the `apply` method. + +## Mutable Types + +A type is called a _mutable_ if it extends `Mutable` and it has an update method or an update class as non-private member or constructor. + +When we create an instance of a mutable type we always add `cap` to its capture set. For instance, if class `Ref` is declared as shown previously then `new Ref(1)` has type `Ref[Int]^{cap}`. + +**Restriction:** A non-mutable type cannot be downcast by a pattern match to a mutable type. + +**Definition:** A class is _read_only_ if the following conditions are met: + + 1. It does not extend any exclusive capabilities from its environment. + 2. It does not take parameters with exclusive capabilities. + 3. It does not contain mutable fields, or fields that take exclusive capabilities. + +**Restriction:** If a class or trait extends `Mutable` all its parent classes or traits must either extend `Mutable` or be read-only. + +The idea is that when we upcast a reference to a type extending `Mutable` to a type that does not extend `Mutable`, we cannot possibly call a method on this reference that uses an exclusive capability. Indeed, by the previous restriction this class must be a read-only class, which means that none of the code implemented +in the class can access exclusive capabilities on its own. And we +also cannot override any of the methods of this class with a method +accessing exclusive capabilities, since such a method would have +to be an update method and update methods are not allowed to override regular methods. + + + +**Example:** + +Consider trait `IterableOnce` from the standard library. + +```scala +trait IterableOnce[+T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + mut def exists(op: T => Boolean): Boolean + ... +``` +The trait is a mutable type with many update methods, among them `foreach` and `exists`. These need to be classified as `mut` because their implementation in the subtrait `Iterator` uses the update method `next`. +```scala +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ... + mut def exists(op; T => Boolean): Boolean = ... + ... +``` +But there are other implementations of `IterableOnce` that are not mutable types (even though they do indirectly extend the `Mutable` trait). Notably, collection classes implement `IterableOnce` by creating a fresh +`iterator` each time one is required. The mutation via `next()` is then restricted to the state of that iterator, whereas the underlying collection is unaffected. These implementations would implement each `mut` method in `IterableOnce` by a normal method without the `mut` modifier. + +```scala +trait Iterable[T] extends IterableOnce[T]: + def iterator = new Iterator[T] { ... } + def foreach(op: T => Unit) = iterator.foreach(op) + def exists(op: T => Boolean) = iterator.exists(op) +``` +Here, `Iterable` is not a mutable type since it has no update method as member. +All inherited update methods are (re-)implemented by normal methods. + +**Note:** One might think that we don't need a base trait `Mutable` since in any case +a mutable type is defined by the presence of update methods, not by what it extends. In fact the importance of `Mutable` is that it defines _the other methods_ as read-only methods that _cannot_ access exclusive capabilities. For types not extending `Mutable`, this is not the case. For instance, the `apply` method of a function type is not an update method and the type itself does not extend `Mutable`. But `apply` may well be implemented by +a method that accesses exclusive capabilities. + + + +## Read-only Capabilities + +If `x` is an exclusive capability of a type extending `Mutable`, `x.rd` is its associated, shared _read-only_ capability. + +`shared` can be understood as the read-only capability corresponding to `cap`. +```scala + shared = cap.rd +``` + +A _top capability_ is either `cap` or `shared`. + + +## Shorthands + +**Meaning of `^`:** + +The meaning of `^` and `=>` is the same as before: + + - `C^` means `C^{cap}`. + - `A => B` means `(A -> B)^{cap}`. + +**Implicitly added capture sets** + +A reference to a type extending any of the traits `Capability` or `Mutable` gets an implicit capture set `{shared}` in case no explicit capture set is given. + +For instance, a matrix multiplication method can be expressed as follows: + +```scala +class Matrix(nrows: Int, ncols: Int) extends Mutable: + mut def update(i: Int, j: Int, x: Double): Unit = ... + def apply(i: Int, j: Int): Double = ... + +def mul(a: Matrix, b: Matrix, c: Matrix^): Unit = + // multiply a and b, storing the result in c +``` +Here, `a` and `b` are implicitly read-only, and `c`'s type has capture set `cap`. I.e. with explicit capture sets this would read: +```scala +def mul(a: Matrix^{shared}, b: Matrix^{shared}, c: Matrix^{cap}): Unit +``` +Separation checking will then make sure that `a` and `b` must be different from `c`. + + +## Capture Sets + +As the previous example showed, we would like to use a `Mutable` type such as `Array` or `Matrix` in two permission levels: read-only and unrestricted. A standard technique is to invent a type qualifier such as "read-only" or "mutable" to indicate access permissions. What we would like to do instead is to combine the qualifier with the capture set of a type. So we +distinguish two kinds of capture sets: regular and read-only. Read-only sets can contain only shared capabilities. + +Internally, in the discussion that follows we use a label after the set to indicate its mode. `{...}_` is regular and `{...}rd` is read-only. We could envisage source language to specify read-only sets, e.g. something like + +```scala +{io, async}.rd +``` + +But in almost all cases we don't need an explicit mode in source code to indicate the kind of capture set, since the contents of the set itself tell us what kind it is. A capture set is assumed to be read-only if it is on a +type extending `Mutable` and it contains only shared capabilities, otherwise it is assumed to be regular. + +The read-only function `ro` maps capture sets to read-only capture sets. It is defined pointwise on capabilities as follows: + + - `ro ({ x1, ..., xn } _) = { ro(x1), ..., ro(xn) }` + - `ro(x) = x` if `x` is shared + - `ro(x) = x.rd` if `x` is exclusive + + + +## Subcapturing + +Subcapturing has to take the mode of capture sets into account. We let `m` stand for arbitrary modes. + +1. Rule (sc-var) comes in two variants. If `x` is defined as `S^C` then + + - `{x, xs} m <: (C u {xs}) m` + - `{x.rd, xs} m <: (ro(C) u {xs}) m` + +3. The subset rule works only between sets of the same kind: + + - `C _ <: C _ u {x}` + - `C rd <: C rd u {x}` if `x` is a shared capability. + +4. We can map regular capture sets to read-only sets: + + - `C _ <: ro(C) rd` + +5. Read-only capabilities in regular capture sets can be widened to exclusive capabilities: + + - `{x.rd, xs} _ <: {x, xs} _` + +One case where an explicit capture set mode would be useful concerns +refinements of type variable bounds, as in the following example. +```scala +class A: + type T <: Object^{x.rd, y} +class B extends A: + type T <: Object^{x.rd} +class C extends B: + type T = Matrix^{x.rd} +``` +We assume that `x` and `y` are exclusive capabilities. +The capture set of type `T` in class `C` is a read-only set since `Matrix` extends `Mutable`. But the capture sets of the occurrences of +`T` in `A` and `B` are regular. This leads to an error in bounds checking +the definition of `T` in `C` against the one in `B` +since read-only sets do not subcapture regular sets. We can fix the +problem by declaring the capture set in class `B` as read-only: +```scala +class B extends A: + type T <: Object^{x.rd}.rd +``` +But now a different problem arises since the capture set of `T` in `B` is +read-only but the capture set of `T` and `A` is regular. The capture set of +`T` in `A` cannot be made read-only since it contains an exclusive capability `y`. So we'd have to drop `y` and declare class `A` like this: +```scala +class A: + type T <: Object^{x.rd}.rd +``` + + + +## Accesses to Mutable Types + +A _read-only access_ is a reference `x` to a type extending `Mutable` with a regular capture set if the expected type is one of the following: + + - a value type that is not a mutable type, or + - a select prototype with a member that is a normal method or class (not an update method or class). + +A read-only access contributes the read-only capability `x.rd` to its environment (as formalized by _cv_). Other accesses contribute the full capability `x`. + +A reference `p.m` to an update method or class `m` of a mutable type is allowed only if `p`'s capture set is regular. + +If `e` is an expression of a type `T^cs` extending `Mutable` and the expected type is a value type that is not a mutable type, then the type of `e` is mapped to `T^ro(cs)`. + + +## Expression Typing + +An expression's type should never contain a top capability in its deep capture set. This is achieved by the following rules: + + - On var access `x`: + + - replace all direct capture sets with `x` + - replace all boxed caps with `x*` + + _Variant_: If the type of the typevar corresponding to a boxed cap can be uniquely reached by a path `this.p`, replace the `cap` with `x.p*`. + + - On select `t.foo` where `C` is the capture set of `t`: apply the SELECT rule, which amounts to: + + - replace all direct caps with `C` + - replace all boxed caps with `C*` + + - On applications: `t(args)`, `new C(args)` if the result type `T` contains `cap` (deeply): + + - create a fresh skolem `val sk: T` + - set result type to `sk.type` + + Skolem symbols are eliminated before they reach the type of the enclosing val or def. + + - When avoiding a variable in a local block, as in: + ```scala + { val x: T^ = ...; ... r: List[T^{x}] } + ``` + where the capture set of `x` contains a top capability, + replace `x` by a fresh skolem `val sk: T`. Alternatively: keep it as is, but don't widen it. + + +## Post Processing Right Hand Sides + +The type of the right hand sides of `val`s or `def`s is post-processed before it becomes the inferred type or is compared with the declared type. Post processing +means that all local skolems in the type are avoided, which might mean `cap` can now occur in the the type. + +However, if a local skolem `sk` has `cap` as underlying type, but is only used +in its read-only form `sk.rd` in the result type, we can drop the skolem instead of widening to `shared`. + +**Example:** + +```scala + def f(x: Int): Double = ... + + def precomputed(n: Int)(f: Int -> Double): Int -> Double = + val a: Array[Double]^ = Array.tabulate(n)(f) + a(_) +``` +Here, `Array.tabulate(n)(f)` returns a value of type `Array[Double]^{cap}`. +The last expression `a(_)` expands to the closure `idx => a(idx)`, which +has type `Int ->{a.rd} Double`, since `a` appears only in the context of a +selection with the `apply` method of `Array`, which is not an update method. The type of the enclosing block then has type `Int ->{sk.rd} Double` for a fresh skolem `sk`, +since `a` is no longer visible. After post processing, this type becomes +`Int -> Double`. + +This pattern allows to use mutation in the construction of a local data structure, returning a pure result when the construction is done. Such +data structures are said to have _transient mutability_. + +## Separation checking + +Separation checking checks that we don't have hidden aliases. A hidden alias arises when we have two definitions `x` and `y` with overlapping transitive capture sets that are not manifest in the types of `x` and `y` because one of these types has widened the alias to a top capability. + +Since expression types can't mention cap, widening happens only + - when passing an argument to a parameter + - when widening to a declared (result) type of a val or def + +**Definitions:** + + - The _transitive capture set_ `tcs(c)` of a capability `c` with underlying capture set `C` is `c` itself, plus the transitive capture set of `C`, but excluding `cap` or `shared`. + + - The _transitive capture set_ `tcs(C)` of a capture set C is the union + of `tcs(c)` for all elements `c` of `C`. + + - Two capture sets _interfere_ if one contains an exclusive capability `x` and the other + also contains `x` or contains the read-only capability `x.rd`. + + - If `C1 <: C2` and `C2` contains a top capability, then let `C2a` be `C2` without top capabilities. The hidden set `hidden(C1, C2)` of `C1` relative to `C2` is the smallest subset `C1h` of `C1` such that `C1 \ C1h <: C2a`. + + - If `T1 <: T2` then let the hidden set `hidden(T1, T2)` of `T1` relative to `T2` be the + union of all hidden sets of corresponding capture sets in `T1` and `T2`. + + +**Algorithm outline:** + + - Associate _shadowed sets_ with blocks, template statement sequences, applications, and val symbols. The idea is that a shadowed set gets populated when a capture reference is widened to cap. In that case the original references that were widened get added to the set. + + - After processing a `val x: T2 = t` with `t: T1` after post-processing: + + - If `T2` is declared, add `tcs(hidden(T1, T2))` to the shadowed set + of the enclosing statement sequence and remember it as `shadowed(x)`. + - If`T2` is inferred, add `tcs(T1)` to the shadowed set + of the enclosing statement sequence and remember it as `shadowed(x)`. + + - When processing the right hand side of a `def f(params): T2 = t` with `t: T1` after post-processing + + - If `T2` is declared, check that `shadowed*(hidden(T1, T2))` contains only local values (including skolems). + - If `T2` is inferred, check that `shadowed*(tcs(T1))` contains only local values (including skolems). + + Here, `shadowed*` is the transitive closure of `shadowed`. + + - When processing an application `p.f(arg1, ..., arg_n)`, after processing `p`, add its transitive capture set to the shadowed set of the call. Then, in sequence, process each argument by adding `tcs(hidden(T1, T2))` to the shadowed set of the call, where `T1` is the argument type and `T2` is the type of the formal parameter. + + - When adding a reference `r` or capture set `C` in `markFree` to enclosing environments, check that `tcs(r)` (respectively, `tcs(C)`) does not interfere with an enclosing shadowed set. + + +This requires, first, a linear processing of the program in evaluation order, and, second, that all capture sets are known. Normal rechecking violates both of these requirements. First, definitions +without declared result types are lazily rechecked using completers. Second, capture sets are constructed +incrementally. So we probably need a second scan after rechecking proper. In order not to duplicate work, we need to record during rechecking all additions to environments via `markFree`. + +**Notes:** + + - Mutable variables are not allowed to have top capabilities in their deep capture sets, so separation checking is not needed for checking var definitions or assignments. + + - A lazy val can be thought of conceptually as a value with possibly a capturing type and as a method computing that value. A reference to a lazy val is interpreted as a call to that method. It's use set is the reference to the lazy val itself as well as the use set of the called method. + + - + +## Escape Checking + +The rules for separation checking also check that capabilities do not escape. Separate +rules for explicitly preventing cap to be boxed or unboxed are not needed anymore. Consider the canonical `withFile` example: +```scala +def withFile[T](body: File^ => T): T = + ... + +withFile: f => + () => f.write("too late") +``` +Here, the argument to `withFile` has the dependent function type +```scala +(f: File^) -> () ->{f} Unit +``` +A non-dependent type is required so the expected result type of the closure is +``` +() ->{cap} Unit +``` +When typing a closure, we type an anonymous function. The result type of that function is determined by type inference. That means the generated closure looks like this +```scala +{ def $anon(f: File^): () ->{cap} Unit = + () => f.write("too late") + $anon +} +``` +By the rules of separation checking the hidden set of the body of $anon is `f`, which refers +to a value outside the rhs of `$anon`. This is illegal according to separation checking. + +In the last example, `f: File^` was an exclusive capability. But it could equally have been a shared capability, i.e. `withFile` could be formulated as follows: +```scala +def withFile[T](body: File^{shared} => T): T = +``` +The same reasoning as before would enforce that there are no leaks. + + +## Mutable Variables + +Local mutable variables are tracked by default. It is essentially as if a mutable variable `x` was decomposed into a new private field of class `Ref` together with a getter and setter. I.e. instead of +```scala +var x: T = init +``` +we'd deal with +```scala +val x$ = Ref[T](init) +def x = x$.get +mut def x_=(y: T) = x$.put(y) +``` + +There should be a way to exclude a mutable variable or field from tracking. Maybe an annotation or modifier such as `transparent` or `untracked`? + +The expansion outlined above justifies the following rules for handling mutable variables directly: + + - A type with non-private tracked mutable fields is classified as mutable. + It has to extend the `Mutable` class. + - A read access to a local mutable variable `x` charges the capability `x.rd` to the environment. + - An assignment to a local mutable variable `x` charges the capability `x` to the environment. + - A read access to a mutable field `this.x` charges the capability `this.rd` to the environment. + - A write access to a mutable field `this.x` charges the capability `this` to the environment. + +Mutable Scopes +============== + +We sometimes want to make separation checking coarser. For instance when constructing a doubly linked list we want to create `Mutable` objects and +store them in mutable variables. Since a variable's type cannot contain `cap`, +we must know beforehand what mutable objects it can be refer to. This is impossible if the other objects are created later. + +Mutable scopes provide a solution to this they permit to derive a set of variables from a common exclusive reference. We define a new class +```scala +class MutableScope extends Mutable +``` +To make mutable scopes useful, we need a small tweak +of the rule governing `new` in the _Mutable Types_ section. The previous rule was: + +> When we create an instance of a mutable type we always add `cap` to its capture set. + +The new rule is: + +> When we create an instance of a mutable type we search for a given value of type `MutableScope`. If such a value is found (say it is `ms`) then we use +`ms` as the capture set of the created instance. Otherwise we use `cap`. + +We could envisage using mutable scopes like this: +``` +object enclave: + private given ms: MutableScope() + + ... +``` +Within `enclave` all mutable objects have `ms` as their capture set. So they can contain variables that also have `ms` as their capture set of their values. + +Mutable scopes should count as mutable types (this can be done either by decree or by adding an update method to `MutableScope`). Hence, mutable scopes can themselves be nested inside other mutable scopes. + +## Consumed Capabilities + +We allow `consume` as a modifier on parameters and methods. Example: + +```scala +class C extends Capability + +class Channel[T]: + def send(consume x: T) + + + +class Buffer[+T] extends Mutable: + consume def append(x: T): Buffer[T]^ + +b.append(x) +b1.append(y) + +def concat[T](consume buf1: Buffer[T]^, buf2: Buffer[T]): Buffer[T]^ + +A ->{x.consume} B + + +A + + C , Gamma, x: S |- t; T + --------------------------- + , Gamma |- (x -> t): S ->C T + + + C, Gamma |- let x = s in t: T + + +class Iterator[T]: + consume def filter(p: T => Boolean): Iterator[T]^ + consume def exists(p: T => Boolean): Boolean +``` + +As a parameter, `consume` implies `^` as capture set of the parameter type. The `^` can be given, but is redundant. + +When a method with a `consume` parameter of type `T2^` is called with an argument of type `T1`, we add the elements of `tcs(hidden(T1, T2^))` not just to the enclosing shadowed set but to all enclosing shadowed sets where elements are visible. This makes these elements permanently inaccessible. + + + +val f = Future { ... } +val g = Future { ... } + + +A parameter is implicitly @unbox if it contains a boxed cap. Example: + +def apply[T](f: Box[T => T], y: T): T = + xs.head(y) + +def compose[T](fs: @unbox List[T => T]) = + xs.foldRight(identity)((f: T => T, g: T => T) => x => g(f(x))) + + + +compose(List(f, g)) + +f :: g :: Nil + +def compose[T](fs: List[Unbox[T => T]], x: T) = + val combined = (xs.foldRight(identity)((f: T => T, g: T => T) => x => g(f(x)))): T->{fs*} T + combined(x) + + +With explicit diff --git a/library/src/scala/annotation/internal/readOnlyCapability.scala b/library/src/scala/annotation/internal/readOnlyCapability.scala new file mode 100644 index 000000000000..8e939aea6bb9 --- /dev/null +++ b/library/src/scala/annotation/internal/readOnlyCapability.scala @@ -0,0 +1,7 @@ +package scala.annotation +package internal + +/** An annotation that marks a capture ref as a read-only capability. + * `x.rd` is encoded as `x.type @readOnlyCapability` + */ +class readOnlyCapability extends StaticAnnotation diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index c35b3b55e813..fb4bacd1a948 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -16,6 +16,8 @@ import annotation.{experimental, compileTimeOnly, retainsCap} @deprecated("Use `Capability` instead") type Cap = Capability + trait Mutable extends Capability + /** Carrier trait for capture set type parameters */ trait CapSet extends Any @@ -41,6 +43,12 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ extension (x: Any) def reachCapability: Any = x + /** Unique capabilities x! which appear as terms in @retains annotations are encoded + * as `caps.uniqueCapability(x)`. When converted to CaptureRef types in capture sets + * they are represented as `x.type @annotation.internal.uniqueCapability`. + */ + extension (x: Any) def readOnlyCapability: Any = x + /** A trait to allow expressing existential types such as * * (x: Exists) => A ->{x} B @@ -52,7 +60,12 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class untrackedCaptures extends annotation.StaticAnnotation - /** This should go into annotations. For now it is here, so that we + /** An annotation on parameters `x` stating that the method's body makes + * use of the reach capability `x*`. Consequently, when calling the method + * we need to charge the deep capture set of the actual argiment to the + * environment. + * + * Note: This should go into annotations. For now it is here, so that we * can experiment with it quickly between minor releases */ final class use extends annotation.StaticAnnotation diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index d4d64424e297..f7b45ddf0eaa 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -1,17 +1,20 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:33 --------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:12 --------------------------------------- 12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? - | ^ - | Found: (f : F) - | Required: File + | ^^^^^^^^^^^^^^^^^^^^^^^ + | Found: (f: F) ->{files.rd*} box Logger{val f²: File^?}^? + | Required: (f: box F^{files.rd*}) => box Logger{val f²: File^?}^? + | + | where: f is a reference to a value parameter + | f² is a value in class Logger | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:15:12 --------------------------------------- 15 | files.map(new Logger(_)) // error, Q: can we improve the error message? | ^^^^^^^^^^^^^ - | Found: (_$1: box File^{files*}) ->{files*} (ex$16: caps.Exists) -> box Logger{val f: File^{_$1}}^{ex$16} - | Required: (_$1: box File^{files*}) => box Logger{val f: File^?}^? + |Found: (_$1: box File^{files*}) ->{files*} (ex$16: caps.Exists) -> box Logger{val f: File^{_$1}}^{ex$16.rd, _$1} + |Required: (_$1: box File^{files*}) => box Logger{val f: File^?}^? | - | Note that the universal capability `cap` - | cannot be included in capture set ? + |Note that reference ex$16.rd + |cannot be included in outer capture set ? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 111719a81f07..bdd053910ac8 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- 36 | try // error | ^ - | The result of `try` cannot have type LazyList[Int]^ since + | The result of `try` cannot have type LazyList[Int]^{cap.rd} since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 37 | tabulate(10) { i => diff --git a/tests/neg-custom-args/captures/mut-outside-mutable.check b/tests/neg-custom-args/captures/mut-outside-mutable.check new file mode 100644 index 000000000000..0407f35745b9 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-outside-mutable.check @@ -0,0 +1,8 @@ +-- Error: tests/neg-custom-args/captures/mut-outside-mutable.scala:5:10 ------------------------------------------------ +5 | mut def foreach(op: T => Unit): Unit // error + | ^ + | Update methods can only be used as members of classes deriving from the `Mutable` trait +-- Error: tests/neg-custom-args/captures/mut-outside-mutable.scala:9:12 ------------------------------------------------ +9 | mut def baz() = 1 // error + | ^ + | Update methods can only be used as members of classes deriving from the `Mutable` trait diff --git a/tests/neg-custom-args/captures/mut-outside-mutable.scala b/tests/neg-custom-args/captures/mut-outside-mutable.scala new file mode 100644 index 000000000000..18c0e59c5bd8 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-outside-mutable.scala @@ -0,0 +1,10 @@ +import caps.Mutable + +trait IterableOnce[T]: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit // error + +trait Foo extends Mutable: + def bar = + mut def baz() = 1 // error + baz() diff --git a/tests/neg-custom-args/captures/mut-override.scala b/tests/neg-custom-args/captures/mut-override.scala new file mode 100644 index 000000000000..848e4d880223 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-override.scala @@ -0,0 +1,19 @@ +import caps.Mutable + +trait IterableOnce[T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ??? + override mut def toString = ??? // error + +trait Iterable[T] extends IterableOnce[T]: + def iterator: Iterator[T] = ??? + def foreach(op: T => Unit) = iterator.foreach(op) + +trait BadIterator[T] extends Iterator[T]: + override mut def hasNext: Boolean // error diff --git a/tests/neg-custom-args/captures/readOnly.check b/tests/neg-custom-args/captures/readOnly.check new file mode 100644 index 000000000000..e1aed07657e5 --- /dev/null +++ b/tests/neg-custom-args/captures/readOnly.check @@ -0,0 +1,19 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/readOnly.scala:14:21 ------------------------------------- +14 | val _: () -> Int = getA // error + | ^^^^ + | Found: (getA : () ->{a.rd} Int) + | Required: () -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/readOnly.scala:17:23 ------------------------------------- +17 | val _: Int -> Unit = putA // error + | ^^^^ + | Found: (putA : (x$0: Int) ->{a} Unit) + | Required: Int -> Unit + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/readOnly.scala:20:23 ---------------------------------------------------------- +20 | val doit = () => z.put(x.get max y.get) // error + | ^^^^^ + | cannot call update method put from (z : Ref), + | since its capture set {z} is read-only diff --git a/tests/neg-custom-args/captures/readOnly.scala b/tests/neg-custom-args/captures/readOnly.scala new file mode 100644 index 000000000000..4edea6638980 --- /dev/null +++ b/tests/neg-custom-args/captures/readOnly.scala @@ -0,0 +1,22 @@ +import caps.Mutable +import caps.cap + +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x + +def Test(c: Object^) = + val a: Ref^ = Ref(1) + val b: Ref^ = Ref(2) + + val getA = () => a.get + val _: () -> Int = getA // error + + val putA = (x: Int) => a.put(x) + val _: Int -> Unit = putA // error + + def setMax(x: Ref^{cap.rd}, y: Ref^{cap.rd}, z: Ref^{cap.rd}) = + val doit = () => z.put(x.get max y.get) // error + val _: () ->{x.rd, y.rd, z} Unit = doit + doit() diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index 7a4b12ac08f6..6b478b48515a 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -7,7 +7,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:14:2 ----------------------------------------------------------- 14 | try // error | ^ - | The result of `try` cannot have type () => Unit since + | The result of `try` cannot have type () ->{cap.rd} Unit since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 15 | () => foo(1) @@ -17,7 +17,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:20:10 ---------------------------------------------------------- 20 | val x = try // error | ^ - | The result of `try` cannot have type () => Unit since + | The result of `try` cannot have type () ->{cap.rd} Unit since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 21 | () => foo(1) @@ -27,7 +27,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:26:10 ---------------------------------------------------------- 26 | val y = try // error | ^ - | The result of `try` cannot have type () => Cell[Unit]^? since + | The result of `try` cannot have type () ->{cap.rd} Cell[Unit]^? since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 27 | () => Cell(foo(1)) @@ -37,8 +37,8 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:32:10 ---------------------------------------------------------- 32 | val b = try // error | ^ - | The result of `try` cannot have type Cell[box () => Unit]^? since - | the part box () => Unit of that type captures the root capability `cap`. + | The result of `try` cannot have type Cell[box () ->{cap.rd} Unit]^? since + | the part box () ->{cap.rd} Unit of that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 33 | Cell(() => foo(1)) 34 | catch diff --git a/tests/pos-custom-args/captures/mutRef.scala b/tests/pos-custom-args/captures/mutRef.scala new file mode 100644 index 000000000000..5fe82c9b987a --- /dev/null +++ b/tests/pos-custom-args/captures/mutRef.scala @@ -0,0 +1,5 @@ +import caps.Mutable +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x diff --git a/tests/pos-custom-args/captures/readOnly.scala b/tests/pos-custom-args/captures/readOnly.scala new file mode 100644 index 000000000000..a550010360a3 --- /dev/null +++ b/tests/pos-custom-args/captures/readOnly.scala @@ -0,0 +1,46 @@ +import caps.Mutable +import caps.cap + +trait Rdr[T]: + def get: T + +class Ref[T](init: T) extends Rdr[T], Mutable: + private var current = init + def get: T = current + mut def put(x: T): Unit = current = x + +def Test(c: Object^) = + val a: Ref[Int]^ = Ref(1) + val b: Ref[Int]^ = Ref(2) + def aa = a + + val getA = () => a.get + val _: () ->{a.rd} Int = getA + + val putA = (x: Int) => a.put(x) + val _: Int ->{a} Unit = putA + + def setMax(x: Ref[Int]^{cap.rd}, y: Ref[Int]^{cap.rd}, z: Ref[Int]^{cap}) = + val doit = () => z.put(x.get max y.get) + val _: () ->{x.rd, y.rd, z} Unit = doit + doit() + + def setMax2(x: Rdr[Int]^{cap.rd}, y: Rdr[Int]^{cap.rd}, z: Ref[Int]^{cap}) = ??? + + setMax2(aa, aa, b) + setMax2(a, aa, b) + + abstract class IMatrix: + def apply(i: Int, j: Int): Double + + class Matrix(nrows: Int, ncols: Int) extends IMatrix, Mutable: + val arr = Array.fill(nrows, ncols)(0.0) + def apply(i: Int, j: Int): Double = arr(i)(j) + mut def update(i: Int, j: Int, x: Double): Unit = arr(i)(j) = x + + def mul(x: IMatrix^{cap.rd}, y: IMatrix^{cap.rd}, z: Matrix^) = ??? + + val m1 = Matrix(10, 10) + val m2 = Matrix(10, 10) + mul(m1, m2, m2) // will fail separation checking + mul(m1, m1, m2) // ok From e4cc265a64d766f801e83b90f4931a9e5168cd3b Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 15 Dec 2024 11:25:15 +0100 Subject: [PATCH 03/93] 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 ad11819878caaf69ce0c2a84540c89a771fb2d94 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 Jan 2025 11:15:30 +0100 Subject: [PATCH 04/93] Implement fresh capabilities These are represented as Fresh.Cap(hidden) where hidden is the set of capabilities subsumed by a fresh. The underlying representation is as an annotated type `T @annotation.internal.freshCapability`. Require -source `3.7` for caps to be converted to Fresh.Cap Also: - Refacture and document CaputureSet - Make SimpleIdentitySets showable - Refactor VarState - Drop Frozen enum - Make VarState subclasses inner classes of companion object - Rename them - Give implicit parameter VarState of subCapture method a default value - Fix printing of capturesets containing cap and some other capability - Revise handing of @uncheckedAnnotation --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 40 +++- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 65 ++++-- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 202 +++++++++++++----- .../dotty/tools/dotc/cc/CheckCaptures.scala | 70 +++--- .../src/dotty/tools/dotc/cc/Existential.scala | 13 +- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 139 ++++++++++++ compiler/src/dotty/tools/dotc/cc/Setup.scala | 54 +++-- .../src/dotty/tools/dotc/cc/Synthetics.scala | 2 +- .../dotty/tools/dotc/core/Definitions.scala | 4 + .../dotty/tools/dotc/core/TypeComparer.scala | 44 ++-- .../src/dotty/tools/dotc/core/TypeOps.scala | 4 +- .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../tools/dotc/printing/Formatting.scala | 5 +- .../tools/dotc/printing/PlainPrinter.scala | 55 +++-- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- .../dotty/tools/dotc/transform/Recheck.scala | 6 +- project/MiMaFilters.scala | 10 + .../captures/explain-under-approx.check | 14 -- 18 files changed, 539 insertions(+), 192 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/Fresh.scala delete mode 100644 tests/neg-custom-args/captures/explain-under-approx.check diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 1a9421aea142..55f8118e9b11 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -16,9 +16,14 @@ import config.Feature import collection.mutable import CCState.* import reporting.Message +import CaptureSet.VarState +/** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() +/** Context property to print Fresh.Cap as "fresh" instead of "cap" */ +val PrintFresh: Key[Unit] = Key() + object ccConfig: /** If true, allow mapping capture set variables under captureChecking with maps that are neither @@ -47,6 +52,10 @@ object ccConfig: def useSealed(using Context) = Feature.sourceVersion.stable != SourceVersion.`3.5` + /** If true, turn on separation checking */ + def useFresh(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`future`) + end ccConfig /** Are we at checkCaptures phase? */ @@ -193,10 +202,7 @@ extension (tp: Type) case tp: TypeParamRef => tp.derivesFrom(defn.Caps_CapSet) case AnnotatedType(parent, annot) => - (annot.symbol == defn.ReachCapabilityAnnot - || annot.symbol == defn.MaybeCapabilityAnnot - || annot.symbol == defn.ReadOnlyCapabilityAnnot - ) && parent.isTrackableRef + defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef case _ => false @@ -244,7 +250,7 @@ extension (tp: Type) * the two capture sets are combined. */ def capturing(cs: CaptureSet)(using Context): Type = - if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK) + if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, VarState.Separate).isOK) && !cs.keepAlways then tp else tp match @@ -421,6 +427,10 @@ extension (tp: Type) mapOver(t) tm(tp) + def hasUseAnnot(using Context): Boolean = tp match + case AnnotatedType(_, ann) => ann.symbol == defn.UseAnnot + case _ => false + /** If `x` is a capture ref, its maybe capability `x?`, represented internally * as `x @maybeCapability`. `x?` stands for a capability `x` that might or might * not be part of a capture set. We have `{} <: {x?} <: {x}`. Maybe capabilities @@ -512,6 +522,24 @@ extension (tp: Type) tp case _ => tp + end withReachCaptures + + /** Does this type contain no-flip covariant occurrences of `cap`? */ + def containsCap(using Context): Boolean = + val acc = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x + || variance > 0 && t.dealiasKeepAnnots.match + case t @ CapturingType(p, cs) if cs.containsCap => + true + case t @ AnnotatedType(parent, ann) => + // Don't traverse annotations, which includes capture sets + this(x, parent) + case Existential(_, _) => + false + case _ => + foldOver(x, t) + acc(false, tp) def level(using Context): Level = tp match @@ -690,7 +718,7 @@ abstract class AnnotatedCapability(annot: Context ?=> ClassSymbol): case _ => AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match - case AnnotatedType(parent: CaptureRef, ann) if ann.symbol == annot => Some(parent) + case AnnotatedType(parent: CaptureRef, ann) if ann.hasSymbol(annot) => Some(parent) case _ => None protected def unwrappable(using Context): Set[Symbol] diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 3a07d88a3ffc..d01fa4d11e4a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -13,6 +13,7 @@ import CCState.* import Periods.NoRunId import compiletime.uninitialized import StdNames.nme +import CaptureSet.VarState /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, * as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities. @@ -78,15 +79,24 @@ trait CaptureRef extends TypeProxy, ValueType: case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false + /** Is this reference a Fresh.Cap instance? */ + final def isFresh(using Context): Boolean = this match + case Fresh.Cap(_) => true + case _ => false + + /** Is this reference the generic root capability `cap` or a Fresh.Cap instance? */ + final def isCapOrFresh(using Context): Boolean = isCap || isFresh + /** Is this reference one the generic root capabilities `cap` or `cap.rd` ? */ final def isRootCapability(using Context): Boolean = this match - case ReadOnlyCapability(tp1) => tp1.isCap - case _ => isCap + case ReadOnlyCapability(tp1) => tp1.isCapOrFresh + case _ => isCapOrFresh /** Is this reference capability that does not derive from another capability ? */ final def isMaxCapability(using Context): Boolean = this match case tp: TermRef => tp.isCap || tp.info.derivesFrom(defn.Caps_Exists) case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + case Fresh.Cap(_) => true case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false @@ -137,34 +147,36 @@ trait CaptureRef extends TypeProxy, ValueType: * Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y * Contains[X, y] ==> X subsumes y * - * TODO: Document cases with more comments. + * TODO: Move to CaptureSet */ - final def subsumes(y: CaptureRef)(using Context): Boolean = + final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = + def subsumingRefs(x: Type, y: Type): Boolean = x match case x: CaptureRef => y match case y: CaptureRef => x.subsumes(y) case _ => false case _ => false - def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.match + def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.dealias match case info: SingletonCaptureRef => test(info) + case CapturingType(parent, _) => + if this.derivesFrom(defn.Caps_CapSet) then test(info) + /* + If `this` is a capture set variable `C^`, then it is possible that it can be + reached from term variables in a reachability chain through the context. + For instance, in `def test[C^](src: Foo^{C^}) = { val x: Foo^{src} = src; val y: Foo^{x} = x; y }` + we expect that `C^` subsumes `x` and `y` in the body of the method + (cf. test case cc-poly-varargs.scala for a more involved example). + */ + else viaInfo(parent)(test) case info: AndType => viaInfo(info.tp1)(test) || viaInfo(info.tp2)(test) case info: OrType => viaInfo(info.tp1)(test) && viaInfo(info.tp2)(test) - case info @ CapturingType(_,_) if this.derivesFrom(defn.Caps_CapSet) => - /* - If `this` is a capture set variable `C^`, then it is possible that it can be - reached from term variables in a reachability chain through the context. - For instance, in `def test[C^](src: Foo^{C^}) = { val x: Foo^{src} = src; val y: Foo^{x} = x; y }` - we expect that `C^` subsumes `x` and `y` in the body of the method - (cf. test case cc-poly-varargs.scala for a more involved example). - */ - test(info) case _ => false (this eq y) - || this.isCap + || maxSubsumes(y, canAddHidden = !vs.isOpen) || y.match - case y: TermRef if !y.isRootCapability => + case y: TermRef if !y.isCap => y.prefix.match case ypre: CaptureRef => this.subsumes(ypre) @@ -213,6 +225,27 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false end subsumes + /** This is a maximal capabaility that subsumes `y` in given context and VarState. + * @param canAddHidden If true we allow maximal capabilties to subsume all other capabilities. + * We add those capabilities to the hidden set if this is Fresh.Cap + * If false we only accept `y` elements that are already in the + * hidden set of this Fresh.Cap. The idea is that in a VarState that + * accepts additions we first run `maxSubsumes` with `canAddHidden = false` + * so that new variables get added to the sets. If that fails, we run + * the test again with canAddHidden = true as a last effort before we + * fail a comparison. + */ + def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = + this.match + case Fresh.Cap(hidden) => + vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) + || !y.stripReadOnly.isCap && canAddHidden && vs.addHidden(hidden, y) + case _ => + this.isCap && canAddHidden + || y.match + case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) + case _ => false + def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index dc6c391b4ca1..863afaa0aaf9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -14,7 +14,6 @@ import printing.{Showable, Printer} import printing.Texts.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.subsumesExistentially import util.common.alwaysTrue import scala.collection.{mutable, immutable} import CCState.* @@ -81,14 +80,26 @@ sealed abstract class CaptureSet extends Showable: assert(!isConst) asInstanceOf[Var] + /** Convert to Const with current elements unconditionally */ + def toConst: Const = this match + case c: Const => c + case v: Var => Const(v.elems) + /** Does this capture set contain the root reference `cap` as element? */ final def isUniversal(using Context) = elems.exists(_.isCap) + /** Does this capture set contain the root reference `cap` as element? */ + final def isUniversalOrFresh(using Context) = + elems.exists(_.isCapOrFresh) + /** Does this capture set contain a root reference `cap` or `cap.rd` as element? */ final def containsRootCapability(using Context) = elems.exists(_.isRootCapability) + final def containsCap(using Context) = + elems.exists(_.stripReadOnly.isCap) + final def isUnboxable(using Context) = elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) @@ -135,8 +146,8 @@ sealed abstract class CaptureSet extends Showable: * element is not the root capability, try instead to include its underlying * capture set. */ - protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if elem.isMaxCapability || summon[VarState] == FrozenState then + protected final def addNewElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elem.isMaxCapability || !vs.isOpen then addThisElem(elem) else addThisElem(elem).orElse: @@ -156,27 +167,40 @@ sealed abstract class CaptureSet extends Showable: */ protected def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult + protected def addHiddenElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elems.exists(_.maxSubsumes(elem, canAddHidden = true)) + then CompareResult.OK + else CompareResult.Fail(this :: Nil) + /** If this is a variable, add `cs` as a dependent set */ protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult /** If `cs` is a variable, add this capture set as one of its dependent sets */ protected def addAsDependentTo(cs: CaptureSet)(using Context): this.type = - cs.addDependent(this)(using ctx, UnrecordedState) + cs.addDependent(this)(using ctx, VarState.Unrecorded) this /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. */ - def accountsFor(x: CaptureRef)(using Context): Boolean = + def accountsFor(x: CaptureRef)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = + def debugInfo(using Context) = i"$this accountsFor $x, which has capture set ${x.captureSetOfInfo}" + def test(using Context) = reporting.trace(debugInfo): elems.exists(_.subsumes(x)) - || !x.isMaxCapability + || // Even though subsumes already follows captureSetOfInfo, this is not enough. + // For instance x: C^{y, z}. Then neither y nor z subsumes x but {y, z} accounts for x. + !x.isMaxCapability && !x.derivesFrom(defn.Caps_CapSet) - && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + && !(vs == VarState.Separate && x.captureSetOfInfo.containsRootCapability) + // in VarState.Separate, don't try to widen to cap since that might succeed with {cap} <: {cap} + && x.captureSetOfInfo.subCaptures(this, VarState.Separate).isOK + comparer match case comparer: ExplainingTypeComparer => comparer.traceIndented(debugInfo)(test) case _ => test + end accountsFor /** A more optimistic version of accountsFor, which does not take variable supersets * of the `x` reference into account. A set might account for `x` if it accounts @@ -186,14 +210,13 @@ sealed abstract class CaptureSet extends Showable: * root capability `cap`. */ def mightAccountFor(x: CaptureRef)(using Context): Boolean = - reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { - elems.exists(_.subsumes(x)) + reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true): + elems.exists(_.subsumes(x)(using ctx, VarState.ClosedUnrecorded)) || !x.isMaxCapability && { val elems = x.captureSetOfInfo.elems !elems.isEmpty && elems.forall(mightAccountFor) } - } /** A more optimistic version of subCaptures used to choose one of two typing rules * for selections and applications. `cs1 mightSubcapture cs2` if `cs2` might account for @@ -209,11 +232,11 @@ sealed abstract class CaptureSet extends Showable: * be added when making this test. An attempt to add either * will result in failure. */ - final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = - subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) + final def subCaptures(that: CaptureSet, vs: VarState)(using Context): CompareResult = + subCaptures(that)(using ctx, vs) /** The subcapturing test, using a given VarState */ - private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = + final def subCaptures(that: CaptureSet)(using ctx: Context, vs: VarState = VarState()): CompareResult = val result = that.tryInclude(elems, this) if result.isOK then addDependent(that) @@ -227,19 +250,22 @@ sealed abstract class CaptureSet extends Showable: * in a frozen state. */ def =:= (that: CaptureSet)(using Context): Boolean = - this.subCaptures(that, frozen = true).isOK - && that.subCaptures(this, frozen = true).isOK + this.subCaptures(that, VarState.Separate).isOK + && that.subCaptures(this, VarState.Separate).isOK /** The smallest capture set (via <:<) that is a superset of both * `this` and `that` */ def ++ (that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then + if this.subCaptures(that, VarState.Separate).isOK then if that.isAlwaysEmpty && this.keepAlways then this else that - else if that.subCaptures(this, frozen = true).isOK then this + else if that.subCaptures(this, VarState.Separate).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) else Union(this, that) + def ++ (that: CaptureSet.Const)(using Context): CaptureSet.Const = + Const(this.elems ++ that.elems) + /** The smallest superset (via <:<) of this capture set that also contains `ref`. */ def + (ref: CaptureRef)(using Context): CaptureSet = @@ -248,8 +274,8 @@ sealed abstract class CaptureSet extends Showable: /** The largest capture set (via <:<) that is a subset of both `this` and `that` */ def **(that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then this - else if that.subCaptures(this, frozen = true).isOK then that + if this.subCaptures(that, VarState.Closed()).isOK then this + else if that.subCaptures(this, VarState.Closed()).isOK then that else if this.isConst && that.isConst then Const(elemIntersection(this, that)) else Intersection(this, that) @@ -366,6 +392,11 @@ sealed abstract class CaptureSet extends Showable: override def toText(printer: Printer): Text = printer.toTextCaptureSet(this) ~~ description + /** Apply function `f` to the elements. Typcially used for printing. + * Overridden in HiddenSet so that we don't run into infinite recursions + */ + def processElems[T](f: Refs => T): T = f(elems) + object CaptureSet: type Refs = SimpleIdentitySet[CaptureRef] type Vars = SimpleIdentitySet[Var] @@ -376,7 +407,7 @@ object CaptureSet: /** If set to `true`, capture stack traces that tell us where sets are created */ private final val debugSets = false - private val emptySet = SimpleIdentitySet.empty + val emptySet = SimpleIdentitySet.empty /** The empty capture set `{}` */ val empty: CaptureSet.Const = Const(emptySet) @@ -385,6 +416,9 @@ object CaptureSet: def universal(using Context): CaptureSet = defn.captureRoot.termRef.singletonCaptureSet + def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = + Fresh.Cap(owner).singletonCaptureSet + /** The shared capture set `{cap.rd}` */ def shared(using Context): CaptureSet = defn.captureRoot.termRef.readOnly.singletonCaptureSet @@ -405,7 +439,7 @@ object CaptureSet: def isAlwaysEmpty = elems.isEmpty def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - CompareResult.Fail(this :: Nil) + addHiddenElem(elem) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -435,7 +469,7 @@ object CaptureSet: object Fluid extends Const(emptySet): override def isAlwaysEmpty = false override def addThisElem(elem: CaptureRef)(using Context, VarState) = CompareResult.OK - override def accountsFor(x: CaptureRef)(using Context): Boolean = true + override def accountsFor(x: CaptureRef)(using Context, VarState): Boolean = true override def mightAccountFor(x: CaptureRef)(using Context): Boolean = true override def toString = "" end Fluid @@ -501,16 +535,16 @@ object CaptureSet: deps = state.deps(this) final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if isConst // Fail if variable is solved, - || !recordElemsState() // or given VarState is frozen, - || Existential.isBadExistential(elem) // or `elem` is an out-of-scope existential, - then + if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen + addHiddenElem(elem) + else if Existential.isBadExistential(elem) then // Fail if `elem` is an out-of-scope existential CompareResult.Fail(this :: Nil) else if !levelOK(elem) then CompareResult.LevelError(this, elem) // or `elem` is not visible at the level of the set. else - //if id == 34 then assert(!elem.isUniversalRootCapability) + // id == 108 then assert(false, i"trying to add $elem to $this") assert(elem.isTrackableRef, elem) + assert(!this.isInstanceOf[HiddenSet] || summon[VarState] == VarState.Separate, summon[VarState]) elems += elem if elem.isRootCapability then rootAddedHandler() @@ -578,7 +612,7 @@ object CaptureSet: this else if isUniversal || computingApprox then universal - else if containsRootCapability && isReadOnly then + else if containsCap && isReadOnly then shared else computingApprox = true @@ -602,11 +636,12 @@ object CaptureSet: */ def solve()(using Context): Unit = if !isConst then - val approx = upperApprox(empty) + val approx = upperApprox(empty).map(Fresh.FromCap(NoSymbol).inverse) .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems - if tryInclude(newElems, empty)(using ctx, VarState()).isOK then + given VarState() + if tryInclude(newElems, empty).isOK then markSolved() /** Mark set as solved and propagate this info to all dependent sets */ @@ -890,6 +925,21 @@ object CaptureSet: def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + /** A capture set variable used to record the references hidden by a Fresh.Cap instance */ + class HiddenSet(initialHidden: Refs = emptySet)(using @constructorOnly ictx: Context) + extends Var(initialElems = initialHidden): + + /** Apply function `f` to `elems` while setting `elems` to empty for the + * duration. This is used to escape infinite recursions if two Frash.Caps + * refer to each other in their hidden sets. + */ + override def processElems[T](f: Refs => T): T = + val savedElems = elems + elems = emptySet + try f(savedElems) + finally elems = savedElems + end HiddenSet + /** Extrapolate tm(r) according to `variance`. Let r1 be the result of tm(r). * - If r1 is a tracked CaptureRef, return {r1} * - If r1 has an empty capture set, return {} @@ -925,7 +975,7 @@ object CaptureSet: */ def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match case TypeBounds(CapturingType(lo, loRefs), CapturingType(hi, hiRefs)) if lo =:= hi => - given VarState = VarState() + given VarState() val cs2 = arg2.captureSet hiRefs.subCaptures(cs2).isOK && cs2.subCaptures(loRefs).isOK case _ => @@ -1001,8 +1051,7 @@ object CaptureSet: def getElems(v: Var): Option[Refs] = elemsMap.get(v) /** Record elements, return whether this was allowed. - * By default, recording is allowed but the special state FrozenState - * overrides this. + * By default, recording is allowed in regular both not in frozen states. */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -1013,36 +1062,78 @@ object CaptureSet: def getDeps(v: Var): Option[Deps] = depsMap.get(v) /** Record dependent sets, return whether this was allowed. - * By default, recording is allowed but the special state FrozenState - * overrides this. + * By default, recording is allowed in regular both not in frozen states. */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } + /** Does this state allow additions of elements to capture set variables? */ + def isOpen = true + + /** Add element to hidden set, recording it in elemsMap, + * return whether this was allowed. By default, recording is allowed + * but the special state VarState.Separate overrides this. + */ + def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = + elemsMap.get(hidden) match + case None => elemsMap(hidden) = hidden.elems + case _ => + hidden.elems += elem + true + /** Roll back global state to what was recorded in this VarState */ def rollBack(): Unit = elemsMap.keysIterator.foreach(_.resetElems()(using this)) depsMap.keysIterator.foreach(_.resetDeps()(using this)) - end VarState - /** A special state that does not allow to record elements or dependent sets. - * In effect this means that no new elements or dependent sets can be added - * in this state (since the previous state cannot be recorded in a snapshot) - */ - @sharable - object FrozenState extends VarState: - override def putElems(v: Var, refs: Refs) = false - override def putDeps(v: Var, deps: Deps) = false - override def rollBack(): Unit = () + private var seen: util.EqHashSet[CaptureRef] = new util.EqHashSet - @sharable - /** A special state that turns off recording of elements. Used only - * in `addSub` to prevent cycles in recordings. - */ - private object UnrecordedState extends VarState: - override def putElems(v: Var, refs: Refs) = true - override def putDeps(v: Var, deps: Deps) = true - override def rollBack(): Unit = () + /** Run test `pred` unless `ref` was seen in an enclosing `ifNotSeen` operation */ + def ifNotSeen(ref: CaptureRef)(pred: => Boolean): Boolean = + if seen.add(ref) then + try pred finally seen -= ref + else false + + object VarState: + /** A class for states that do not allow to record elements or dependent sets. + * In effect this means that no new elements or dependent sets can be added + * in these states (since the previous state cannot be recorded in a snapshot) + * On the other hand, these states do allow by default Fresh.Cap instances to + * subsume arbitary types, which are then recorded in their hidden sets. + */ + class Closed extends VarState: + override def putElems(v: Var, refs: Refs) = false + override def putDeps(v: Var, deps: Deps) = false + override def isOpen = false + + /** A closed state that allows a Fresh.Cap instance to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. + */ + @sharable + object Separate extends Closed: + override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = false + + /** A special state that turns off recording of elements. Used only + * in `addSub` to prevent cycles in recordings. + */ + @sharable + private[CaptureSet] object Unrecorded extends VarState: + override def putElems(v: Var, refs: Refs) = true + override def putDeps(v: Var, deps: Deps) = true + override def rollBack(): Unit = () + override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = true + + /** A closed state that turns off recording of hidden elements (but allows + * adding them). Used in `mightAccountFor`. + */ + @sharable + private[CaptureSet] object ClosedUnrecorded extends Closed: + override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = true + + end VarState + + @sharable /** The current VarState, as passed by the implicit context */ def varState(using state: VarState): VarState = state @@ -1117,6 +1208,9 @@ object CaptureSet: case CapturingType(parent, refs) => recur(parent) ++ refs case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) => + // Note: we don't use the `ReachCapability(parent)` extractor here since that + // only works if `parent` is a CaptureRef, but in illegal programs it might not be. + // And then we do not want to fall back to empty. parent match case parent: SingletonCaptureRef if parent.isTrackableRef => tp.singletonCaptureSet @@ -1167,7 +1261,7 @@ object CaptureSet: case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => seen += t.symbol val upper = t.info.bounds.hi - if includeTypevars && upper.isExactlyAny then CaptureSet.universal + if includeTypevars && upper.isExactlyAny then CaptureSet.fresh(t.symbol) else this(cs, upper) case t @ FunctionOrMethod(args, res @ Existential(_, _)) if args.forall(_.isAlwaysPure) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index eab11d03144d..0b1d2397629b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -18,11 +18,12 @@ import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* import scala.collection.mutable -import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult, VarState} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} +import Existential.derivedExistentialType /** The capture checker */ object CheckCaptures: @@ -88,6 +89,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsMap" end SubstParamsMap /** Used for substituting parameters in a special case: when all actual arguments @@ -107,6 +109,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsBiMap" lazy val inverse = new BiTypeMap: def apply(tp: Type): Type = tp match @@ -123,6 +126,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsBiMap.inverse" def inverse = thisMap end SubstParamsBiMap @@ -307,32 +311,33 @@ class CheckCaptures extends Recheck, SymTransformer: /** Assert subcapturing `cs1 <: cs2` (available for debugging, otherwise unused) */ def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = - assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + assert(cs1.subCaptures(cs2).isOK, i"$cs1 is not a subset of $cs2") /** If `res` is not CompareResult.OK, report an error */ - def checkOK(res: CompareResult, prefix: => String, pos: SrcPos, provenance: => String = "")(using Context): Unit = + def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = if !res.isOK then - def toAdd: String = CaptureSet.levelErrors.toAdd.mkString - def descr: String = - val d = res.blocking.description - if d.isEmpty then provenance else "" - report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) + inContext(Fresh.printContext(added, res.blocking)): + def toAdd: String = CaptureSet.levelErrors.toAdd.mkString + def descr: String = + val d = res.blocking.description + if d.isEmpty then provenance else "" + report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) /** Check subcapturing `{elem} <: cs`, report error on failure */ def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = checkOK( - elem.singletonCaptureSet.subCaptures(cs, frozen = false), + elem.singletonCaptureSet.subCaptures(cs), i"$elem cannot be referenced here; it is not", - pos, provenance) + elem, pos, provenance) /** Check subcapturing `cs1 <: cs2`, report error on failure */ def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, provenance: => String = "", cs1description: String = "")(using Context) = checkOK( - cs1.subCaptures(cs2, frozen = false), + cs1.subCaptures(cs2), if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not" else i"references $cs1$cs1description are not all", - pos, provenance) + cs1, pos, provenance) /** If `sym` is a class or method nested inside a term, a capture set variable representing * the captured variables of the environment associated with `sym`. @@ -635,11 +640,11 @@ class CheckCaptures extends Recheck, SymTransformer: val meth = tree.fun.symbol if meth == defn.Caps_unsafeAssumePure then val arg :: Nil = tree.args: @unchecked - val argType0 = recheck(arg, pt.capturing(CaptureSet.universal)) + val argType0 = recheck(arg, pt.stripCapturing.capturing(CaptureSet.universal)) val argType = if argType0.captureSet.isAlwaysEmpty then argType0 else argType0.widen.stripCapturing - capt.println(i"rechecking $arg with $pt: $argType") + capt.println(i"rechecking unsafeAssumePure of $arg with $pt: $argType") super.recheckFinish(argType, tree, pt) else val res = super.recheckApply(tree, pt) @@ -650,13 +655,13 @@ class CheckCaptures extends Recheck, SymTransformer: * charge the deep capture set of the actual argument to the environment. */ protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = - val argType = recheck(arg, formal) - formal match - case AnnotatedType(formal1, ann) if ann.symbol == defn.UseAnnot => - // The UseAnnot is added to `formal` by `prepareFunction` - capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") - markFree(argType.deepCaptureSet, arg.srcPos) - case _ => + val freshenedFormal = Fresh.fromCap(formal) + val argType = recheck(arg, freshenedFormal) + .showing(i"recheck arg $arg vs $freshenedFormal", capt) + if formal.hasUseAnnot then + // The @use annotation is added to `formal` by `prepareFunction` + capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") + markFree(argType.deepCaptureSet, arg.srcPos) argType /** Map existential captures in result to `cap` and implement the following @@ -686,9 +691,7 @@ class CheckCaptures extends Recheck, SymTransformer: val qualCaptures = qualType.captureSet val argCaptures = for (argType, formal) <- argTypes.lazyZip(funType.paramInfos) yield - formal match - case AnnotatedType(_, ann) if ann.symbol == defn.UseAnnot => argType.deepCaptureSet - case _ => argType.captureSet + if formal.hasUseAnnot then argType.deepCaptureSet else argType.captureSet appType match case appType @ CapturingType(appType1, refs) if qualType.exists @@ -746,8 +749,8 @@ class CheckCaptures extends Recheck, SymTransformer: def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = - if core.derivesFromMutable then CaptureSet.universal - else if core.derivesFromCapability then initCs ++ defn.universalCSImpliedByCapability + if core.derivesFromMutable then CaptureSet.fresh() + else if core.derivesFromCapability then initCs ++ Fresh.Cap().readOnly.singletonCaptureSet else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol @@ -768,6 +771,8 @@ class CheckCaptures extends Recheck, SymTransformer: // can happen for curried constructors if instantiate of a previous step // added capture set to result. augmentConstructorType(parent, initCs ++ refs) + case core @ Existential(boundVar, core1) => + core.derivedExistentialType(augmentConstructorType(core1, initCs)) case _ => val (refined, cs) = addParamArgRefinements(core, initCs) refined.capturing(cs) @@ -1201,10 +1206,11 @@ class CheckCaptures extends Recheck, SymTransformer: actualBoxed else capt.println(i"conforms failed for ${tree}: $actual vs $expected") - err.typeMismatch(tree.withType(actualBoxed), expected1, - addApproxAddenda( - addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), - expected1)) + inContext(Fresh.printContext(actualBoxed, expected1)): + err.typeMismatch(tree.withType(actualBoxed), expected1, + addApproxAddenda( + addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), + expected1)) actual end checkConformsExpr @@ -1370,7 +1376,7 @@ class CheckCaptures extends Recheck, SymTransformer: val cs = actual.captureSet if covariant then cs ++ leaked else - if !leaked.subCaptures(cs, frozen = false).isOK then + if !leaked.subCaptures(cs).isOK then report.error( em"""$expected cannot be box-converted to ${actual.capturing(leaked)} |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos) @@ -1693,7 +1699,7 @@ class CheckCaptures extends Recheck, SymTransformer: val widened = ref.captureSetOfInfo val added = widened.filter(isAllowed(_)) capt.println(i"heal $ref in $cs by widening to $added") - if !added.subCaptures(cs, frozen = false).isOK then + if !added.subCaptures(cs).isOK then val location = if meth.exists then i" of ${meth.showLocated}" else "" val paramInfo = if ref.paramName.info.kind.isInstanceOf[UniqueNameKind] diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 19800a12a05c..39f6fcf14fd9 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -242,10 +242,10 @@ object Existential: case _ => core - /** Map top-level existentials to `cap`. */ + /** Map top-level existentials to `Fresh.Cap`. */ def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - unpacked.substParam(boundVar, defn.captureRoot.termRef) + unpacked.substParam(boundVar, Fresh.Cap()) case tp1 @ CapturingType(parent, refs) => tp1.derivedCapturingType(toCap(parent), refs) case tp1 @ AnnotatedType(parent, ann) => @@ -256,7 +256,7 @@ object Existential: */ def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - toCapDeeply(unpacked.substParam(boundVar, defn.captureRoot.termRef)) + toCapDeeply(unpacked.substParam(boundVar, Fresh.Cap())) case tp1 @ FunctionOrMethod(args, res) => val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) if tp2 ne tp1 then tp2 else tp @@ -273,7 +273,7 @@ object Existential: case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) case _ => false - /** Replace all occurrences of `cap` in parts of this type by an existentially bound + /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound * variable. If there are such occurrences, or there might be in the future due to embedded * capture set variables, create an existential with the variable wrapping the type. * Stop at function or method types since these have been mapped before. @@ -294,7 +294,7 @@ object Existential: class Wrap(boundVar: TermParamRef) extends CapMap: def apply(t: Type) = t match - case t: TermRef if t.isCap => + case t: CaptureRef if t.isCapOrFresh => // !!! we should map different fresh refs to different existentials if variance > 0 then needsWrap = true boundVar @@ -317,8 +317,9 @@ object Existential: //.showing(i"mapcap $t = $result") lazy val inverse = new BiTypeMap: + lazy val freshCap = Fresh.Cap() def apply(t: Type) = t match - case t: TermParamRef if t eq boundVar => defn.captureRoot.termRef + case t: TermParamRef if t eq boundVar => freshCap case _ => mapOver(t) def inverse = Wrap.this override def toString = "Wrap.inverse" diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala new file mode 100644 index 000000000000..14c4c03e4115 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -0,0 +1,139 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import StdNames.nme +import ast.tpd.* +import Decorators.* +import typer.ErrorReporting.errorType +import Names.TermName +import NameKinds.ExistentialBinderName +import NameOps.isImpureFunction +import reporting.Message +import util.SimpleIdentitySet.empty +import CaptureSet.{Refs, emptySet, NarrowingCapabilityMap} +import dotty.tools.dotc.util.SimpleIdentitySet + +/** Handling fresh in CC: + +*/ +object Fresh: + + case class Annot(hidden: CaptureSet.HiddenSet) extends Annotation: + override def symbol(using Context) = defn.FreshCapabilityAnnot + override def tree(using Context) = New(symbol.typeRef, Nil) + override def derivedAnnotation(tree: Tree)(using Context): Annotation = this + + override def hash: Int = hidden.hashCode + override def eql(that: Annotation) = that match + case Annot(hidden) => this.hidden eq hidden + case _ => false + end Annot + + private def ownerToHidden(owner: Symbol, reach: Boolean)(using Context): Refs = + val ref = owner.termRef + if reach then + if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet + else + if ref.isTracked then SimpleIdentitySet(ref) else emptySet + + object Cap: + + def apply(initialHidden: Refs = emptySet)(using Context): CaptureRef = + if ccConfig.useFresh then + AnnotatedType(defn.captureRoot.termRef, Annot(CaptureSet.HiddenSet(initialHidden))) + else + defn.captureRoot.termRef + + def apply(owner: Symbol, reach: Boolean)(using Context): CaptureRef = + apply(ownerToHidden(owner, reach)) + + def apply(owner: Symbol)(using Context): CaptureRef = + apply(ownerToHidden(owner, reach = false)) + + def unapply(tp: AnnotatedType)(using Context): Option[CaptureSet.HiddenSet] = tp.annot match + case Annot(hidden) => Some(hidden) + case _ => None + end Cap + + class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: + thisMap => + + var reach = false + + private def initHidden = + val ref = owner.termRef + if reach then + if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet + else + if ref.isTracked then SimpleIdentitySet(ref) else emptySet + + override def apply(t: Type) = + if variance <= 0 then t + else t match + case t: CaptureRef if t.isCap => + Cap(initHidden) + case t @ CapturingType(_, refs) => + val savedReach = reach + if t.isBoxed then reach = true + try mapOver(t) finally reach = savedReach + case t @ AnnotatedType(parent, ann) => + val parent1 = this(parent) + if ann.symbol.isRetains && ann.tree.toCaptureSet.containsCap then + this(CapturingType(parent1, ann.tree.toCaptureSet)) + else + t.derivedAnnotatedType(parent1, ann) + case _ => + mapFollowingAliases(t) + + override def toString = "CapToFresh" + + lazy val inverse: BiTypeMap & FollowAliasesMap = new BiTypeMap with FollowAliasesMap: + def apply(t: Type): Type = t match + case t @ Cap(_) => defn.captureRoot.termRef + case t @ CapturingType(_, refs) => mapOver(t) + case _ => mapFollowingAliases(t) + + def inverse = thisMap + override def toString = thisMap.toString + ".inverse" + + end FromCap + + /** Maps cap to fresh */ + def fromCap(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = + if ccConfig.useFresh then FromCap(owner)(tp) else tp + + /** Maps fresh to cap */ + def toCap(tp: Type)(using Context): Type = + if ccConfig.useFresh then FromCap(NoSymbol).inverse(tp) else tp + + /** If `refs` contains an occurrence of `cap` or `cap.rd`, the current context + * with an added property PrintFresh. This addition causes all occurrences of + * `Fresh.Cap` to be printed as `fresh` instead of `cap`, so that one avoids + * confusion in error messages. + */ + def printContext(refs: (Type | CaptureSet)*)(using Context): Context = + def hasCap = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x || t.dealiasKeepAnnots.match + case Fresh.Cap(_) => false + case t: TermRef => t.isCap || this(x, t.widen) + case x: ThisType => false + case _ => foldOver(x, t) + def containsFresh(x: Type | CaptureSet): Boolean = x match + case tp: Type => + hasCap(false, tp) + case refs: CaptureSet => + refs.elems.exists(_.stripReadOnly.isCap) + + if refs.exists(containsFresh) then ctx.withProperty(PrintFresh, Some(())) + else ctx + end printContext +end Fresh + + + + + diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 8a353778c44e..e6ab50dfb632 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -132,7 +132,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def mappedInfo = if toBeUpdated.contains(sym) then symd.info // don't transform symbols that will anyway be updated - else transformExplicitType(symd.info) + else Fresh.fromCap(transformExplicitType(symd.info), sym) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) else if isPreCC(sym) then @@ -356,6 +356,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: catch case ex: IllegalCaptureRef => report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) parent2 + else if ann.symbol == defn.UncheckedCapturesAnnot then + makeUnchecked(apply(parent)) else t.derivedAnnotatedType(parent1, ann) case throwsAlias(res, exc) => @@ -428,20 +430,30 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts: import checker.* - /** Transform type of tree, and remember the transformed type as the type the tree */ - private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = + private val paramSigChange = util.EqHashSet[Tree]() + + /** Transform type of tree, and remember the transformed type as the type the tree + * @pre !(boxed && sym.exists) + */ + private def transformTT(tree: TypeTree, sym: Symbol, boxed: Boolean)(using Context): Unit = if !tree.hasNuType then - val transformed = + var transformed = if tree.isInferred then transformInferredType(tree.tpe) else transformExplicitType(tree.tpe, tptToCheck = tree) - tree.setNuType(if boxed then box(transformed) else transformed) + if boxed then transformed = box(transformed) + if sym.is(Param) && (transformed ne tree.tpe) then + paramSigChange += tree + tree.setNuType( + if boxed then transformed + else if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) + else Fresh.fromCap(transformed, sym)) /** Transform the type of a val or var or the result type of a def */ def transformResultType(tpt: TypeTree, sym: Symbol)(using Context): Unit = // First step: Transform the type and record it as knownType of tpt. try - transformTT(tpt, + transformTT(tpt, sym, boxed = sym.isMutableVar && !ccConfig.useSealed @@ -490,9 +502,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree @ TypeApply(fn, args) => traverse(fn) - if !defn.isTypeTestOrCast(fn.symbol) then - for case arg: TypeTree <- args do - transformTT(arg, boxed = true) // type arguments in type applications are boxed + for case arg: TypeTree <- args do + if defn.isTypeTestOrCast(fn.symbol) then + arg.setNuType(Fresh.fromCap(arg.tpe)) + else + transformTT(arg, NoSymbol, boxed = true) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => val sym = tree.symbol @@ -501,6 +515,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: inContext(ctx.withOwner(sym)) traverseChildren(tree) + case tree @ TypeDef(_, rhs: TypeTree) => + transformTT(rhs, tree.symbol, boxed = false) + case tree @ SeqLiteral(elems, tpt: TypeTree) => traverse(elems) tpt.setNuType(box(transformInferredType(tpt.tpe))) @@ -517,7 +534,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Processing done on node `tree` after its children are traversed */ def postProcess(tree: Tree)(using Context): Unit = tree match case tree: TypeTree => - transformTT(tree, boxed = false) + transformTT(tree, NoSymbol, boxed = false) case tree: ValOrDefDef => // Make sure denotation of tree's symbol is correct val sym = tree.symbol @@ -544,8 +561,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def paramSignatureChanges = tree.match case tree: DefDef => tree.paramss.nestedExists: - case param: ValDef => param.tpt.hasNuType - case param: TypeDef => param.rhs.hasNuType + case param: ValDef => paramSigChange.contains(param.tpt) + case param: TypeDef => paramSigChange.contains(param.rhs) case _ => false // A symbol's signature changes if some of its parameter types or its result type @@ -580,7 +597,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: mt.paramInfos else val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) - psyms.map(psym => adaptedInfo(psym, subst(psym.nextInfo).asInstanceOf[mt.PInfo])), + psyms.map(psym => adaptedInfo(psym, subst(Fresh.toCap(psym.nextInfo)).asInstanceOf[mt.PInfo])), mt1 => integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas) ) @@ -798,6 +815,16 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if variance > 0 then t1 else decorate(t1, Function.const(CaptureSet.Fluid)) + /** Replace all universal capture sets in this type by */ + private def makeUnchecked(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def apply(t: Type) = t match + case t @ CapturingType(parent, refs) => + val parent1 = this(parent) + if refs.isUniversal then t.derivedCapturingType(parent1, CaptureSet.Fluid) + else t + case Existential(_) => t + case _ => mapFollowingAliases(t) + /** Pull out an embedded capture set from a part of `tp` */ def normalizeCaptures(tp: Type)(using Context): Type = tp match case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => @@ -877,6 +904,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: for j <- 0 until retained.length if j != i r <- retained(j).toCaptureRefs + if !r.isMaxCapability yield r val remaining = CaptureSet(others*) check(remaining, remaining) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 1372ebafe82f..9e2729eb7f31 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -116,7 +116,7 @@ object Synthetics: def transformUnapplyCaptures(info: Type)(using Context): Type = info match case info: MethodType => val paramInfo :: Nil = info.paramInfos: @unchecked - val newParamInfo = CapturingType(paramInfo, CaptureSet.universal) + val newParamInfo = CapturingType(paramInfo, CaptureSet.fresh()) val trackedParam = info.paramRefs.head def newResult(tp: Type): Type = tp match case tp: MethodOrPoly => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index ac9f42a6fad0..7e6f795c661e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1085,6 +1085,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") @@ -1556,6 +1557,9 @@ class Definitions { @tu lazy val pureSimpleClasses = Set(StringClass, NothingClass, NullClass) ++ ScalaValueClasses() + @tu lazy val capabilityWrapperAnnots: Set[Symbol] = + Set(ReachCapabilityAnnot, ReadOnlyCapabilityAnnot, MaybeCapabilityAnnot, FreshCapabilityAnnot) + @tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0).asInstanceOf[Array[TypeRef]] val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(AbstractFunctionType.map(_.symbol.asClass)) def AbstractFunctionClass(n: Int)(using Context): Symbol = AbstractFunctionClassPerRun()(using ctx)(n) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 73e53138829b..e4e2c6359eab 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -440,7 +440,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if (tp1.prefix.isStable) return tryLiftedToThis1 case _ => if isCaptureVarComparison then - return subCaptures(tp1.captureSet, tp2.captureSet, frozenConstraint).isOK + return subCaptures(tp1.captureSet, tp2.captureSet).isOK if (tp1 eq NothingType) || isBottom(tp1) then return true } @@ -548,7 +548,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1 @ CapturingType(parent1, refs1) => def compareCapturing = if tp2.isAny then true - else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) + else if subCaptures(refs1, tp2.captureSet).isOK && sameBoxed(tp1, tp2, refs1) || !ctx.mode.is(Mode.CheckBoundsOrSelfType) && tp1.isAlwaysPure then val tp2a = @@ -591,7 +591,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && (isBottom(tp1) || GADTusage(tp2.symbol)) if isCaptureVarComparison then - return subCaptures(tp1.captureSet, tp2.captureSet, frozenConstraint).isOK + return subCaptures(tp1.captureSet, tp2.captureSet).isOK isSubApproxHi(tp1, info2.lo) && (trustBounds || isSubApproxHi(tp1, info2.hi)) || compareGADT @@ -678,12 +678,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) case (info1 @ CapturingType(parent1, refs1), info2: Type) if info2.stripCapturing.isInstanceOf[MethodOrPoly] => - subCaptures(refs1, info2.captureSet, frozenConstraint).isOK && sameBoxed(info1, info2, refs1) + subCaptures(refs1, info2.captureSet).isOK && sameBoxed(info1, info2, refs1) && isSubInfo(parent1, info2) case (info1: Type, CapturingType(parent2, refs2)) if info1.stripCapturing.isInstanceOf[MethodOrPoly] => val refs1 = info1.captureSet - (refs1.isAlwaysEmpty || subCaptures(refs1, refs2, frozenConstraint).isOK) && sameBoxed(info1, info2, refs1) + (refs1.isAlwaysEmpty || subCaptures(refs1, refs2).isOK) && sameBoxed(info1, info2, refs1) && isSubInfo(info1, parent2) case _ => isSubType(info1, info2) @@ -877,12 +877,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // capt-capibility.scala and function-combinators.scala val singletonOK = tp1 match case tp1: SingletonType - if subCaptures(tp1.underlying.captureSet, refs2, frozen = true).isOK => + if subCaptures(tp1.underlying.captureSet, refs2, CaptureSet.VarState.Separate).isOK => recur(tp1.widen, tp2) case _ => false singletonOK - || subCaptures(refs1, refs2, frozenConstraint).isOK + || subCaptures(refs1, refs2).isOK && sameBoxed(tp1, tp2, refs1) && (recur(tp1.widen.stripCapturing, parent2) || tp1.isInstanceOf[SingletonType] && recur(tp1, parent2) @@ -2896,29 +2896,30 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling end inverse end MapExistentials - protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + protected def makeVarState() = + if frozenConstraint then CaptureSet.VarState.Closed() else CaptureSet.VarState() + + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, + vs: CaptureSet.VarState = makeVarState())(using Context): CaptureSet.CompareResult = try if assocExistentials.isEmpty then - refs1.subCaptures(refs2, frozen) + refs1.subCaptures(refs2, vs) else val mapped = refs1.map(MapExistentials(assocExistentials)) if mapped.elems.exists(Existential.isBadExistential) then CaptureSet.CompareResult.Fail(refs2 :: Nil) - else subCapturesMapped(mapped, refs2, frozen) + else mapped.subCaptures(refs2, vs) catch case ex: AssertionError => println(i"fail while subCaptures $refs1 <:< $refs2") throw ex - protected def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - refs1.subCaptures(refs2, frozen) - /** Is the boxing status of tp1 and tp2 the same, or alternatively, is * the capture sets `refs1` of `tp1` a subcapture of the empty set? * In the latter case, boxing status does not matter. */ protected def sameBoxed(tp1: Type, tp2: Type, refs1: CaptureSet)(using Context): Boolean = (tp1.isBoxedCapturing == tp2.isBoxedCapturing) - || refs1.subCaptures(CaptureSet.empty, frozenConstraint).isOK + || refs1.subCaptures(CaptureSet.empty, makeVarState()).isOK // ----------- Diagnostics -------------------------------------------------- @@ -3496,8 +3497,8 @@ object TypeComparer { def reduceMatchWith[T](op: MatchReducer => T)(using Context): T = comparing(_.reduceMatchWith(op)) - def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - comparing(_.subCaptures(refs1, refs2, frozen)) + def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = + comparing(_.subCaptures(refs1, refs2, vs)) def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = comparing(_.subsumesExistentially(tp1, tp2)) @@ -3978,14 +3979,9 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa super.gadtAddBound(sym, b, isUpper) } - override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { - super.subCaptures(refs1, refs2, frozen) - } - - override def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures mapped $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { - super.subCapturesMapped(refs1, refs2, frozen) + override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = + traceIndented(i"subcaptures $refs1 <:< $refs2, varState = ${vs.toString}") { + super.subCaptures(refs1, refs2, vs) } def lastTrace(header: String): String = header + { try b.toString finally b.clear() } diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index a7f41a71d7ce..0b758061febd 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -19,7 +19,7 @@ import typer.Inferencing.* import typer.IfBottom import reporting.TestingReporter import cc.{CapturingType, derivedCapturingType, CaptureSet, captureSet, isBoxed, isBoxedCapturing} -import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} +import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap, VarState} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -161,7 +161,7 @@ object TypeOps: TypeComparer.lub(simplify(l, theMap), simplify(r, theMap), isSoft = tp.isSoft) case tp @ CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) - && refs.subCaptures(parent.captureSet, frozen = true).isOK + && refs.subCaptures(parent.captureSet, VarState.Separate).isOK && (tp.isBoxed || !parent.isBoxedCapturing) // fuse types with same boxed status and outer boxed with any type then diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 7c0c89da97ee..c9defa97d6ff 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4175,7 +4175,7 @@ object Types extends TypeUtils { tl => params.map(p => tl.integrate(params, adaptParamInfo(p))), tl => tl.integrate(params, resultType)) - /** Adapt info of parameter symbol to be integhrated into corresponding MethodType + /** Adapt info of parameter symbol to be integrated into corresponding MethodType * using the scheme described in `fromSymbols`. */ def adaptParamInfo(param: Symbol, pinfo: Type)(using Context): Type = diff --git a/compiler/src/dotty/tools/dotc/printing/Formatting.scala b/compiler/src/dotty/tools/dotc/printing/Formatting.scala index ccd7b4e4e282..741b997d9926 100644 --- a/compiler/src/dotty/tools/dotc/printing/Formatting.scala +++ b/compiler/src/dotty/tools/dotc/printing/Formatting.scala @@ -8,7 +8,7 @@ import core.* import Texts.*, Types.*, Flags.*, Symbols.*, Contexts.* import Decorators.* import reporting.Message -import util.DiffUtil +import util.{DiffUtil, SimpleIdentitySet} import Highlighting.* object Formatting { @@ -87,6 +87,9 @@ object Formatting { def show(x: H *: T) = CtxShow(toStr(x.head) *: toShown(x.tail).asInstanceOf[Tuple]) + given [X <: AnyRef: Show]: Show[SimpleIdentitySet[X]] with + def show(x: SimpleIdentitySet[X]) = summon[Show[List[X]]].show(x.toList) + given Show[FlagSet] with def show(x: FlagSet) = x.flagsString diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 0f8e81154058..94656cc33bb2 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -27,6 +27,12 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def printDebug = ctx.settings.YprintDebug.value + /** Print Fresh.Cap instances as */ + protected def printFreshDetailed = printDebug + + /** Print Fresh.Cap instances as "fresh" */ + protected def printFresh = printFreshDetailed || ctx.property(PrintFresh).isDefined + private var openRecs: List[RecType] = Nil protected def maxToTextRecursions: Int = 100 @@ -153,12 +159,14 @@ class PlainPrinter(_ctx: Context) extends Printer { + defn.FromJavaObjectSymbol def toTextCaptureSet(cs: CaptureSet): Text = - if printDebug && ctx.settings.YccDebug.value && !cs.isConst then cs.toString + if printDebug && ctx.settings.YccDebug.value + && !cs.isConst && !cs.isInstanceOf[CaptureSet.HiddenSet] //HiddenSets can be cyclic + then cs.toString else if cs == CaptureSet.Fluid then "" else val core: Text = if !cs.isConst && cs.elems.isEmpty then "?" - else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + else "{" ~ Text(cs.processElems(_.toList.map(toTextCaptureRef)), ", ") ~ "}" // ~ Str("?").provided(!cs.isConst) core ~ cs.optionalInfo @@ -202,14 +210,14 @@ class PlainPrinter(_ctx: Context) extends Printer { else toTextPrefixOf(tp) ~ selectionString(tp) case tp: TermParamRef => - ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ ".type" + ParamRefNameString(tp) ~ hashStr(tp.binder) ~ ".type" case tp: TypeParamRef => val suffix = if showNestingLevel then val tvar = ctx.typerState.constraint.typeVarOfParam(tp) if tvar.exists then s"#${tvar.asInstanceOf[TypeVar].nestingLevel.toString}" else "" else "" - ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ suffix + ParamRefNameString(tp) ~ hashStr(tp.binder) ~ suffix case tp: SingletonType => toTextSingleton(tp) case AppliedType(tycon, args) => @@ -248,9 +256,12 @@ class PlainPrinter(_ctx: Context) extends Printer { toText(parent) else val refsText = - if refs.isUniversal && (refs.elems.size == 1 || !printDebug) - then rootSetText - else toTextCaptureSet(refs) + if refs.isUniversal then + if refs.elems.size == 1 then rootSetText else toTextCaptureSet(refs) + else if !refs.elems.isEmpty && refs.elems.forall(_.isCapOrFresh) && !printFresh then + rootSetText + else + toTextCaptureSet(refs) toTextCapturing(parent, refsText, boxText) case tp @ RetainingType(parent, refs) => if Feature.ccEnabledSomewhere then @@ -282,19 +293,19 @@ class PlainPrinter(_ctx: Context) extends Printer { case ExprType(restp) => def arrowText: Text = restp match case AnnotatedType(parent, ann) if ann.symbol == defn.RetainsByNameAnnot => - val refs = ann.tree.retainedElems - if refs.exists(_.symbol == defn.captureRoot) then Str("=>") - else Str("->") ~ toTextRetainedElems(refs) + ann.tree.retainedElems match + case ref :: Nil if ref.symbol == defn.captureRoot => Str("=>") + case refs => Str("->") ~ toTextRetainedElems(refs) case _ => if Feature.pureFunsEnabled then "->" else "=>" changePrec(GlobalPrec)(arrowText ~ " " ~ toText(restp)) case tp: HKTypeLambda => changePrec(GlobalPrec) { - "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) + "[" ~ paramsText(tp) ~ "]" ~ hashStr(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) } case tp: PolyType => changePrec(GlobalPrec) { - "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ + "[" ~ paramsText(tp) ~ "]" ~ hashStr(tp) ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) ~ toTextGlobal(tp.resultType) } @@ -345,7 +356,7 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def paramsText(lam: LambdaType): Text = { def paramText(ref: ParamRef) = val erased = ref.underlying.hasAnnotation(defn.ErasedParamAnnot) - keywordText("erased ").provided(erased) ~ ParamRefNameString(ref) ~ lambdaHash(lam) ~ toTextRHS(ref.underlying, isParameter = true) + keywordText("erased ").provided(erased) ~ ParamRefNameString(ref) ~ hashStr(lam) ~ toTextRHS(ref.underlying, isParameter = true) Text(lam.paramRefs.map(paramText), ", ") } @@ -357,11 +368,11 @@ class PlainPrinter(_ctx: Context) extends Printer { /** The name of the symbol without a unique id. */ protected def simpleNameString(sym: Symbol): String = nameString(sym.name) - /** If -uniqid is set, the hashcode of the lambda type, after a # */ - protected def lambdaHash(pt: LambdaType): Text = - if (showUniqueIds) - try "#" + pt.hashCode - catch { case ex: NullPointerException => "" } + /** If -uniqid is set, the hashcode of the type, after a # */ + protected def hashStr(tp: Type): String = + if showUniqueIds then + try "#" + tp.hashCode + catch case ex: NullPointerException => "" else "" /** A string to append to a symbol composed of: @@ -410,7 +421,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp @ ConstantType(value) => toText(value) case pref: TermParamRef => - ParamRefNameString(pref) ~ lambdaHash(pref.binder) + ParamRefNameString(pref) ~ hashStr(pref.binder) case tp: RecThis => val idx = openRecs.reverse.indexOf(tp.binder) if (idx >= 0) selfRecName(idx + 1) @@ -424,12 +435,16 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextCaptureRef(tp: Type): Text = homogenize(tp) match - case tp: TermRef if tp.symbol == defn.captureRoot => Str("cap") + case tp: TermRef if tp.symbol == defn.captureRoot => "cap" case tp: SingletonType => toTextRef(tp) case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^" case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" + case Fresh.Cap(hidden) => + if printFreshDetailed then s"" + else if printFresh then "fresh" + else "cap" case tp => toText(tp) protected def isOmittablePrefix(sym: Symbol): Boolean = diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 4c1da6ed1e1d..372b2ac095d1 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -337,7 +337,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "?" ~ (("(ignored: " ~ toText(ignored) ~ ")") provided printDebug) case tp @ PolyProto(targs, resType) => "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) - case ReachCapability(_) | MaybeCapability(_) | ReadOnlyCapability(_) => + case tp: AnnotatedType if tp.isTrackableRef => toTextCaptureRef(tp) case _ => super.toText(tp) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 8936c460de81..e8227f759ad4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -167,7 +167,11 @@ abstract class Recheck extends Phase, SymTransformer: * from the current type. */ def setNuType(tpe: Type): Unit = - if nuTypes.lookup(tree) == null && (tpe ne tree.tpe) then nuTypes(tree) = tpe + if nuTypes.lookup(tree) == null then updNuType(tpe) + + /** Set new type of the tree unconditionally. */ + def updNuType(tpe: Type): Unit = + if tpe ne tree.tpe then nuTypes(tree) = tpe /** The new type of the tree, or if none was installed, the original type */ def nuType(using Context): Type = diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index e4473b9869ca..3f225482b2c4 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -8,6 +8,16 @@ object MiMaFilters { val ForwardsBreakingChanges: Map[String, Seq[ProblemFilter]] = Map( // Additions that require a new minor version of the library Build.mimaPreviousDottyVersion -> Seq( + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.betterFors"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$betterFors$"), + 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"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.preview"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.packageObjectValues"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$packageObjectValues$"), ), // Additions since last LTS diff --git a/tests/neg-custom-args/captures/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check deleted file mode 100644 index f84ac5eb2b53..000000000000 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ /dev/null @@ -1,14 +0,0 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:12:10 ------------------------- -12 | col.add(Future(() => 25)) // error - | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async)}^{async} - | Required: Future[Int]^{col.futs*} - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 ------------------------- -15 | col1.add(Future(() => 25)) // error - | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async)}^{async} - | Required: Future[Int]^{col1.futs*} - | - | longer explanation available when compiling with `-explain` From 4c8a50f3d731482ebdcadf7c996aceddd19f6419 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 Jan 2025 19:56:29 +0100 Subject: [PATCH 05/93] Separation checking for applications Check separation from source 3.7 on. We currently only check applications, other areas of separation checking are still to be implemented. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 23 ++ .../src/dotty/tools/dotc/cc/SepCheck.scala | 202 ++++++++++++++++++ .../src/dotty/tools/dotc/cc/Synthetics.scala | 8 +- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../annotation/internal/freshCapability.scala | 7 + library/src/scala/caps.scala | 5 + .../src/scala/collection/IterableOnce.scala | 2 +- .../immutable/LazyListIterable.scala | 14 +- .../captures/box-adapt-cases.check | 15 +- .../captures/box-adapt-cases.scala | 3 +- .../captures/caseclass/Test_2.scala | 2 +- .../captures/cc-subst-param-exact.scala | 6 +- .../captures/depfun-reach.check | 4 +- .../captures/depfun-reach.scala | 2 +- .../captures/existential-mapping.check | 24 +-- .../captures/existential-mapping.scala | 2 +- .../captures/filevar-expanded.check | 19 ++ .../captures/filevar-expanded.scala | 4 +- tests/neg-custom-args/captures/filevar.check | 9 + tests/neg-custom-args/captures/i19330.check | 7 + tests/neg-custom-args/captures/i19330.scala | 6 +- tests/neg-custom-args/captures/i21614.check | 2 +- tests/neg-custom-args/captures/i21614.scala | 2 +- tests/neg-custom-args/captures/lazyref.check | 31 ++- tests/neg-custom-args/captures/lazyref.scala | 3 +- .../neg-custom-args/captures/outer-var.check | 22 +- .../neg-custom-args/captures/outer-var.scala | 1 + tests/neg-custom-args/captures/reaches.check | 14 ++ tests/neg-custom-args/captures/reaches.scala | 6 +- .../captures/sep-compose.check | 120 +++++++++++ .../captures/sep-compose.scala | 45 ++++ .../captures/sepchecks.scala} | 22 +- .../captures/unsound-reach-2.scala | 4 +- .../captures/unsound-reach-3.scala | 4 +- .../captures/unsound-reach-4.check | 7 + .../captures/unsound-reach-4.scala | 4 +- .../captures/unsound-reach.check | 7 + .../captures/unsound-reach.scala | 4 +- .../captures/update-call.scala | 19 ++ tests/neg-custom-args/captures/vars.check | 7 +- tests/neg-custom-args/captures/vars.scala | 2 +- .../captures/boxmap-paper.scala | 5 +- .../captures/cc-dep-param.scala | 3 +- tests/pos-custom-args/captures/foreach2.scala | 7 + .../captures/nested-classes-2.scala | 18 +- .../captures/sep-compose.scala | 21 ++ tests/pos-custom-args/captures/sep-eq.scala | 20 ++ .../captures/simple-apply.scala | 6 + tests/pos-custom-args/captures/skolems2.scala | 15 ++ tests/pos-special/stdlib/Test2.scala | 9 +- .../colltest5/CollectionStrawManCC5_1.scala | 24 ++- .../captures/colltest5/Test_2.scala | 6 +- 53 files changed, 712 insertions(+), 115 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/SepCheck.scala create mode 100644 library/src/scala/annotation/internal/freshCapability.scala create mode 100644 tests/neg-custom-args/captures/filevar-expanded.check rename tests/{pos-custom-args => neg-custom-args}/captures/filevar-expanded.scala (90%) create mode 100644 tests/neg-custom-args/captures/filevar.check create mode 100644 tests/neg-custom-args/captures/sep-compose.check create mode 100644 tests/neg-custom-args/captures/sep-compose.scala rename tests/{pos-custom-args/captures/readOnly.scala => neg-custom-args/captures/sepchecks.scala} (66%) create mode 100644 tests/neg-custom-args/captures/update-call.scala create mode 100644 tests/pos-custom-args/captures/foreach2.scala create mode 100644 tests/pos-custom-args/captures/sep-compose.scala create mode 100644 tests/pos-custom-args/captures/sep-eq.scala create mode 100644 tests/pos-custom-args/captures/simple-apply.scala create mode 100644 tests/pos-custom-args/captures/skolems2.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 55f8118e9b11..49eb73dd762e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -54,7 +54,7 @@ object ccConfig: /** If true, turn on separation checking */ def useFresh(using Context): Boolean = - Feature.sourceVersion.stable.isAtLeast(SourceVersion.`future`) + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) end ccConfig diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 0b1d2397629b..d494bc8d9e22 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -242,6 +242,17 @@ object CheckCaptures: /** Was a new type installed for this tree? */ def hasNuType: Boolean + + /** Is this tree passed to a parameter or assigned to a value with a type + * that contains cap in no-flip covariant position, which will necessite + * a separation check? + */ + def needsSepCheck: Boolean + + /** If a tree is an argument for which needsSepCheck is true, + * the type of the formal paremeter corresponding to the argument. + */ + def formalType: Type end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -282,6 +293,15 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val todoAtPostCheck = new mutable.ListBuffer[() => Unit] + /** Maps trees that need a separation check because they are arguments to + * polymorphic parameters. The trees are mapped to the formal parameter type. + */ + private val sepCheckFormals = util.EqHashMap[Tree, Type]() + + extension [T <: Tree](tree: T) + def needsSepCheck: Boolean = sepCheckFormals.contains(tree) + def formalType: Type = sepCheckFormals.getOrElse(tree, NoType) + /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. */ @@ -662,6 +682,8 @@ class CheckCaptures extends Recheck, SymTransformer: // The @use annotation is added to `formal` by `prepareFunction` capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") markFree(argType.deepCaptureSet, arg.srcPos) + if formal.containsCap then + sepCheckFormals(arg) = freshenedFormal argType /** Map existential captures in result to `cap` and implement the following @@ -1786,6 +1808,7 @@ class CheckCaptures extends Recheck, SymTransformer: end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) + if ccConfig.useFresh then SepChecker(this).traverse(unit) if !ctx.reporter.errorsReported then // We dont report errors here if previous errors were reported, because other // errors often result in bad applied types, but flagging these bad types gives diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala new file mode 100644 index 000000000000..9f5e8187d1d0 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -0,0 +1,202 @@ +package dotty.tools +package dotc +package cc +import ast.tpd +import collection.mutable + +import core.* +import Symbols.*, Types.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import CaptureSet.{Refs, emptySet} +import config.Printers.capt +import StdNames.nme + +class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: + import tpd.* + import checker.* + + extension (refs: Refs) + private def footprint(using Context): Refs = + def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match + case newElem :: newElems1 => + val superElems = newElem.captureSetOfInfo.elems.filter: superElem => + !superElem.isMaxCapability && !elems.contains(superElem) + recur(elems ++ superElems, newElems1 ++ superElems.toList) + case Nil => elems + val elems: Refs = refs.filter(!_.isMaxCapability) + recur(elems, elems.toList) + + private def overlapWith(other: Refs)(using Context): Refs = + val refs1 = refs + val refs2 = other + def common(refs1: Refs, refs2: Refs) = + refs1.filter: ref => + ref.isExclusive && refs2.exists(_.stripReadOnly eq ref) + common(refs, other) ++ common(other, refs) + + private def hidden(refs: Refs)(using Context): Refs = + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + def hiddenByElem(elem: CaptureRef): Refs = + if seen.add(elem) then elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) + case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) + case _ => emptySet + else emptySet + + def recur(cs: Refs): Refs = + (emptySet /: cs): (elems, elem) => + elems ++ hiddenByElem(elem) + + recur(refs) + end hidden + + /** The captures of an argument or prefix widened to the formal parameter, if + * the latter contains a cap. + */ + private def formalCaptures(arg: Tree)(using Context): Refs = + val argType = arg.formalType.orElse(arg.nuType) + (if arg.nuType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) + .elems + + /** The captures of an argument of prefix. No widening takes place */ + private def actualCaptures(arg: Tree)(using Context): Refs = + val argType = arg.nuType + (if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) + .elems + + private def sepError(fn: Tree, args: List[Tree], argIdx: Int, + overlap: Refs, hiddenInArg: Refs, footprints: List[(Refs, Int)], + deps: collection.Map[Tree, List[Tree]])(using Context): Unit = + val arg = args(argIdx) + def paramName(mt: Type, idx: Int): Option[Name] = mt match + case mt @ MethodType(pnames) => + if idx < pnames.length then Some(pnames(idx)) else paramName(mt.resType, idx - pnames.length) + case mt: PolyType => paramName(mt.resType, idx) + case _ => None + def formalName = paramName(fn.nuType.widen, argIdx) match + case Some(pname) => i"$pname " + case _ => "" + def whatStr = if overlap.size == 1 then "this capability is" else "these capabilities are" + def funStr = + if fn.symbol.exists then i"${fn.symbol}: ${fn.symbol.info}" + else i"a function of type ${fn.nuType.widen}" + val clashIdx = footprints + .collect: + case (fp, idx) if !hiddenInArg.overlapWith(fp).isEmpty => idx + .head + def whereStr = clashIdx match + case 0 => "function prefix" + case 1 => "first argument " + case 2 => "second argument" + case 3 => "third argument " + case n => s"${n}th argument " + def clashTree = + if clashIdx == 0 then methPart(fn).asInstanceOf[Select].qualifier + else args(clashIdx - 1) + def clashType = clashTree.nuType + def clashCaptures = actualCaptures(clashTree) + def hiddenCaptures = hidden(formalCaptures(arg)) + def clashFootprint = clashCaptures.footprint + def hiddenFootprint = hiddenCaptures.footprint + def declaredFootprint = deps(arg).map(actualCaptures(_)).foldLeft(emptySet)(_ ++ _).footprint + def footprintOverlap = hiddenFootprint.overlapWith(clashFootprint) -- declaredFootprint + report.error( + em"""Separation failure: argument of type ${arg.nuType} + |to $funStr + |corresponds to capture-polymorphic formal parameter ${formalName}of type ${arg.formalType} + |and captures ${CaptureSet(overlap)}, but $whatStr also passed separately + |in the ${whereStr.trim} with type $clashType. + | + | Capture set of $whereStr : ${CaptureSet(clashCaptures)} + | Hidden set of current argument : ${CaptureSet(hiddenCaptures)} + | Footprint of $whereStr : ${CaptureSet(clashFootprint)} + | Hidden footprint of current argument : ${CaptureSet(hiddenFootprint)} + | Declared footprint of current argument: ${CaptureSet(declaredFootprint)} + | Undeclared overlap of footprints : ${CaptureSet(footprintOverlap)}""", + arg.srcPos) + end sepError + + private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = + val fnCaptures = methPart(fn) match + case Select(qual, _) => qual.nuType.captureSet + case _ => CaptureSet.empty + capt.println(i"check separate $fn($args), fnCaptures = $fnCaptures, argCaptures = ${args.map(arg => CaptureSet(formalCaptures(arg)))}, deps = ${deps.toList}") + var footprint = fnCaptures.elems.footprint + val footprints = mutable.ListBuffer[(Refs, Int)]((footprint, 0)) + val indexedArgs = args.zipWithIndex + + def subtractDeps(elems: Refs, arg: Tree): Refs = + deps(arg).foldLeft(elems): (elems, dep) => + elems -- actualCaptures(dep).footprint + + for (arg, idx) <- indexedArgs do + if !arg.needsSepCheck then + footprint = footprint ++ subtractDeps(actualCaptures(arg).footprint, arg) + footprints += ((footprint, idx + 1)) + for (arg, idx) <- indexedArgs do + if arg.needsSepCheck then + val ac = formalCaptures(arg) + val hiddenInArg = hidden(ac).footprint + //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") + val overlap = subtractDeps(hiddenInArg.overlapWith(footprint), arg) + if !overlap.isEmpty then + sepError(fn, args, idx, overlap, hiddenInArg, footprints.toList, deps) + footprint ++= actualCaptures(arg).footprint + footprints += ((footprint, idx + 1)) + end checkApply + + private def collectMethodTypes(tp: Type): List[TermLambda] = tp match + case tp: MethodType => tp :: collectMethodTypes(tp.resType) + case tp: PolyType => collectMethodTypes(tp.resType) + case _ => Nil + + private def dependencies(fn: Tree, argss: List[List[Tree]])(using Context): collection.Map[Tree, List[Tree]] = + val mtpe = + if fn.symbol.exists then fn.symbol.info + else fn.tpe.widen // happens for PolyFunction applies + val mtps = collectMethodTypes(mtpe) + assert(mtps.hasSameLengthAs(argss), i"diff for $fn: ${fn.symbol} /// $mtps /// $argss") + val mtpsWithArgs = mtps.zip(argss) + val argMap = mtpsWithArgs.toMap + val deps = mutable.HashMap[Tree, List[Tree]]().withDefaultValue(Nil) + for + (mt, args) <- mtpsWithArgs + (formal, arg) <- mt.paramInfos.zip(args) + dep <- formal.captureSet.elems.toList + do + val referred = dep match + case dep: TermParamRef => + argMap(dep.binder)(dep.paramNum) :: Nil + case dep: ThisType if dep.cls == fn.symbol.owner => + val Select(qual, _) = fn: @unchecked + qual :: Nil + case _ => + Nil + deps(arg) ++= referred + deps + + private def traverseApply(tree: Tree, argss: List[List[Tree]])(using Context): Unit = tree match + case Apply(fn, args) => traverseApply(fn, args :: argss) + case TypeApply(fn, args) => traverseApply(fn, argss) // skip type arguments + case _ => + if argss.nestedExists(_.needsSepCheck) then + checkApply(tree, argss.flatten, dependencies(tree, argss)) + + def traverse(tree: Tree)(using Context): Unit = + tree match + case tree: GenericApply => + if tree.symbol != defn.Caps_unsafeAssumeSeparate then + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree, Nil) + traverseChildren(tree) + case _ => + traverseChildren(tree) +end SepChecker + + + + + + diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 9e2729eb7f31..cfdcbbc401bf 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -132,8 +132,9 @@ object Synthetics: val (pt: PolyType) = info: @unchecked val (mt: MethodType) = pt.resType: @unchecked val (enclThis: ThisType) = owner.thisType: @unchecked + val paramCaptures = CaptureSet(enclThis, defn.captureRoot.termRef) pt.derivedLambdaType(resType = MethodType(mt.paramNames)( - mt1 => mt.paramInfos.map(_.capturing(CaptureSet.universal)), + mt1 => mt.paramInfos.map(_.capturing(paramCaptures)), mt1 => CapturingType(mt.resType, CaptureSet(enclThis, mt1.paramRefs.head)))) def transformCurriedTupledCaptures(info: Type, owner: Symbol) = @@ -148,7 +149,10 @@ object Synthetics: ExprType(mapFinalResult(et.resType, CapturingType(_, CaptureSet(enclThis)))) def transformCompareCaptures = - MethodType(defn.ObjectType.capturing(CaptureSet.universal) :: Nil, defn.BooleanType) + val (enclThis: ThisType) = symd.owner.thisType: @unchecked + MethodType( + defn.ObjectType.capturing(CaptureSet(defn.captureRoot.termRef, enclThis)) :: Nil, + defn.BooleanType) symd.copySymDenotation(info = symd.name match case DefaultGetterName(nme.copy, n) => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 7e6f795c661e..0a08c974090d 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1006,6 +1006,7 @@ class Definitions { @tu lazy val Caps_Exists: ClassSymbol = requiredClass("scala.caps.Exists") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") + @tu lazy val Caps_unsafeAssumeSeparate: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumeSeparate") @tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_containsImpl: TermSymbol = CapsModule.requiredMethod("containsImpl") @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") diff --git a/library/src/scala/annotation/internal/freshCapability.scala b/library/src/scala/annotation/internal/freshCapability.scala new file mode 100644 index 000000000000..a25eee4f4c6d --- /dev/null +++ b/library/src/scala/annotation/internal/freshCapability.scala @@ -0,0 +1,7 @@ +package scala.annotation +package internal + +/** An annotation used internally for fresh capability wrappers of `cap` + */ +class freshCapability extends StaticAnnotation + diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index fb4bacd1a948..9d0a8883cde9 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -79,4 +79,9 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ def unsafeAssumePure: T = x + /** A wrapper around code for which separation checks are suppressed. + */ + def unsafeAssumeSeparate[T](op: T): T = op + end unsafe +end caps \ No newline at end of file diff --git a/scala2-library-cc/src/scala/collection/IterableOnce.scala b/scala2-library-cc/src/scala/collection/IterableOnce.scala index 7e8555421c53..7ea62a9e1a65 100644 --- a/scala2-library-cc/src/scala/collection/IterableOnce.scala +++ b/scala2-library-cc/src/scala/collection/IterableOnce.scala @@ -805,7 +805,7 @@ trait IterableOnceOps[+A, +CC[_], +C] extends Any { this: IterableOnce[A]^ => case _ => Some(reduceLeft(op)) } private final def reduceLeftOptionIterator[B >: A](op: (B, A) => B): Option[B] = reduceOptionIterator[A, B](iterator)(op) - private final def reduceOptionIterator[X >: A, B >: X](it: Iterator[X]^)(op: (B, X) => B): Option[B] = { + private final def reduceOptionIterator[X >: A, B >: X](it: Iterator[X]^{this, caps.cap})(op: (B, X) => B): Option[B] = { if (it.hasNext) { var acc: B = it.next() while (it.hasNext) diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 28ce8da104aa..cae2f4299e87 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -25,6 +25,7 @@ import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures import caps.untrackedCaptures +import caps.unsafe.unsafeAssumeSeparate /** This class implements an immutable linked list. We call it "lazy" * because it computes its elements only when they are needed. @@ -879,6 +880,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz if (!cursor.stateDefined) b.append(sep).append("") } else { @inline def same(a: LazyListIterable[A]^, b: LazyListIterable[A]^): Boolean = (a eq b) || (a.state eq b.state) + // !!!CC with qualifiers, same should have cap.rd parameters // Cycle. // If we have a prefix of length P followed by a cycle of length C, // the scout will be at position (P%C) in the cycle when the cursor @@ -890,7 +892,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // the start of the loop. var runner = this var k = 0 - while (!same(runner, scout)) { + while (!unsafeAssumeSeparate(same(runner, scout))) { runner = runner.tail scout = scout.tail k += 1 @@ -900,11 +902,11 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // everything once. If cursor is already at beginning, we'd better // advance one first unless runner didn't go anywhere (in which case // we've already looped once). - if (same(cursor, scout) && (k > 0)) { + if (unsafeAssumeSeparate(same(cursor, scout)) && (k > 0)) { appendCursorElement() cursor = cursor.tail } - while (!same(cursor, scout)) { + while (!unsafeAssumeSeparate(same(cursor, scout))) { appendCursorElement() cursor = cursor.tail } @@ -1052,7 +1054,9 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { val head = it.next() rest = rest.tail restRef = rest // restRef.elem = rest - sCons(head, newLL(stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state))) + sCons(head, newLL( + unsafeAssumeSeparate( + stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state)))) } else State.Empty } } @@ -1181,7 +1185,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { def iterate[A](start: => A)(f: A => A): LazyListIterable[A]^{start, f} = newLL { val head = start - sCons(head, iterate(f(head))(f)) + sCons(head, unsafeAssumeSeparate(iterate(f(head))(f))) } /** diff --git a/tests/neg-custom-args/captures/box-adapt-cases.check b/tests/neg-custom-args/captures/box-adapt-cases.check index 7ff185c499a5..e5cadb051ac1 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.check +++ b/tests/neg-custom-args/captures/box-adapt-cases.check @@ -1,12 +1,19 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:14:10 ------------------------------ -14 | x.value(cap => cap.use()) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:8:10 ------------------------------- +8 | x.value(cap => cap.use()) // error, was OK + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) => Int + | Required: (cap: box Cap^) ->{fresh} Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:15:10 ------------------------------ +15 | x.value(cap => cap.use()) // error | ^^^^^^^^^^^^^^^^ | Found: (cap: box Cap^?) ->{io} Int | Required: (cap: box Cap^{io}) -> Int | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:28:10 ------------------------------ -28 | x.value(cap => cap.use()) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:29:10 ------------------------------ +29 | x.value(cap => cap.use()) // error | ^^^^^^^^^^^^^^^^ | Found: (cap: box Cap^?) ->{io, fs} Int | Required: (cap: box Cap^{io, fs}) ->{io} Int diff --git a/tests/neg-custom-args/captures/box-adapt-cases.scala b/tests/neg-custom-args/captures/box-adapt-cases.scala index 8f7d7a0a6667..55371c4e50b7 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.scala +++ b/tests/neg-custom-args/captures/box-adapt-cases.scala @@ -1,10 +1,11 @@ +import language.`3.7` // sepchecks on trait Cap { def use(): Int } def test1(): Unit = { class Id[X](val value: [T] -> (op: X => T) -> T) val x: Id[Cap^] = ??? - x.value(cap => cap.use()) + x.value(cap => cap.use()) // error, was OK } def test2(io: Cap^): Unit = { diff --git a/tests/neg-custom-args/captures/caseclass/Test_2.scala b/tests/neg-custom-args/captures/caseclass/Test_2.scala index e54ab1774202..8c13a0d831ef 100644 --- a/tests/neg-custom-args/captures/caseclass/Test_2.scala +++ b/tests/neg-custom-args/captures/caseclass/Test_2.scala @@ -5,7 +5,7 @@ def test(c: C) = val mixed: () ->{c} Unit = pure val x = Ref(impure) val _: Ref = x // error - val y = x.copy() + val y = caps.unsafe.unsafeAssumeSeparate(x.copy()) // TODO remove val yc: Ref = y // error val y0 = x.copy(pure) val yc0: Ref = y0 diff --git a/tests/neg-custom-args/captures/cc-subst-param-exact.scala b/tests/neg-custom-args/captures/cc-subst-param-exact.scala index 35e4acb95fdc..08a3efaaffdf 100644 --- a/tests/neg-custom-args/captures/cc-subst-param-exact.scala +++ b/tests/neg-custom-args/captures/cc-subst-param-exact.scala @@ -5,13 +5,13 @@ trait Ref[T] { def set(x: T): T } def test() = { def swap[T](x: Ref[T]^)(y: Ref[T]^{x}): Unit = ??? - def foo[T](x: Ref[T]^): Unit = + def foo[T](x: Ref[T]^{cap.rd}): Unit = swap(x)(x) - def bar[T](x: () => Ref[T]^)(y: Ref[T]^{x}): Unit = + def bar[T](x: () => Ref[T]^{cap.rd})(y: Ref[T]^{x}): Unit = swap(x())(y) // error - def baz[T](x: Ref[T]^)(y: Ref[T]^{x}): Unit = + def baz[T](x: Ref[T]^{cap.rd})(y: Ref[T]^{x}): Unit = swap(x)(y) } diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index c1d7d05dc8d6..676ca7c5104f 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -2,13 +2,13 @@ 13 | op // error | ^^ | Found: (xs: List[(X, box () ->{io} Unit)]) ->{op} List[box () ->{xs*} Unit] - | Required: (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] + | Required: (xs: List[(X, box () ->{io} Unit)]) ->{fresh} List[() -> Unit] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:20:60 --------------------------------- 20 | val b: (xs: List[() ->{io} Unit]) => List[() ->{} Unit] = a // error | ^ | Found: (xs: List[box () ->{io} Unit]) ->{a} List[box () ->{xs*} Unit] - | Required: (xs: List[box () ->{io} Unit]) => List[() -> Unit] + | Required: (xs: List[box () ->{io} Unit]) ->{fresh} List[() -> Unit] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/depfun-reach.scala b/tests/neg-custom-args/captures/depfun-reach.scala index 94b10f7dbcdb..6c198ff8fd9f 100644 --- a/tests/neg-custom-args/captures/depfun-reach.scala +++ b/tests/neg-custom-args/captures/depfun-reach.scala @@ -1,6 +1,6 @@ import language.experimental.captureChecking import caps.cap - +import language.`3.7` // sepchecks on def test(io: Object^, async: Object^) = def compose(op: List[(() ->{cap} Unit, () ->{cap} Unit)]): List[() ->{op*} Unit] = List(() => op.foreach((f,g) => { f(); g() })) diff --git a/tests/neg-custom-args/captures/existential-mapping.check b/tests/neg-custom-args/captures/existential-mapping.check index 30836bc427cf..b52fdb5750ed 100644 --- a/tests/neg-custom-args/captures/existential-mapping.check +++ b/tests/neg-custom-args/captures/existential-mapping.check @@ -47,42 +47,42 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:27:25 -------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) => (ex$41: caps.Exists) -> C^{ex$41}) - | Required: (x: C^) => C + | Found: (y1 : (x: C^) ->{fresh} (ex$41: caps.Exists) -> C^{ex$41}) + | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:30:20 -------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ => (ex$45: caps.Exists) -> C^{ex$45}) - | Required: C^ => C + | Found: (y2 : C^ ->{fresh} (ex$45: caps.Exists) -> C^{ex$45}) + | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:33:30 -------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ => (ex$50: caps.Exists) -> (x: C^) ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) - | Required: A^ => (ex$53: caps.Exists) -> (x: C^) ->{ex$53} C + | Found: (y3 : A^ ->{fresh} (ex$50: caps.Exists) -> (x: C^) ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) + | Required: A^ ->{fresh} (ex$53: caps.Exists) -> (x: C^) ->{ex$53} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:36:25 -------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ => (ex$56: caps.Exists) -> C^ ->{ex$56} (ex$55: caps.Exists) -> C^{ex$55}) - | Required: A^ => (ex$59: caps.Exists) -> C^ ->{ex$59} C + | Found: (y4 : A^ ->{fresh} (ex$56: caps.Exists) -> C^ ->{ex$56} (ex$55: caps.Exists) -> C^{ex$55}) + | Required: A^ ->{fresh} (ex$59: caps.Exists) -> C^ ->{ex$59} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:39:30 -------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ => (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) - | Required: A^ => (x: C^) -> C + | Found: (y5 : A^ ->{fresh} (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) + | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:42:30 -------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ => (ex$70: caps.Exists) -> (x: C^) ->{ex$70} (ex$69: caps.Exists) -> C^{ex$69}) - | Required: A^ => (ex$73: caps.Exists) -> (x: C^) ->{ex$73} C + | Found: (y6 : A^ ->{fresh} (ex$70: caps.Exists) -> (x: C^) ->{ex$70} (ex$69: caps.Exists) -> C^{ex$69}) + | Required: A^ ->{fresh} (ex$73: caps.Exists) -> (x: C^) ->{ex$73} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/existential-mapping.scala b/tests/neg-custom-args/captures/existential-mapping.scala index 290f7dc767a6..aa45e60cdabc 100644 --- a/tests/neg-custom-args/captures/existential-mapping.scala +++ b/tests/neg-custom-args/captures/existential-mapping.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking - +import language.`3.7` // sepchecks on class A class C type Fun[X] = (x: C^) -> X diff --git a/tests/neg-custom-args/captures/filevar-expanded.check b/tests/neg-custom-args/captures/filevar-expanded.check new file mode 100644 index 000000000000..e1991890f6fa --- /dev/null +++ b/tests/neg-custom-args/captures/filevar-expanded.check @@ -0,0 +1,19 @@ +-- Error: tests/neg-custom-args/captures/filevar-expanded.scala:34:19 -------------------------------------------------- +34 | withFile(io3): f => // error: separation failure + | ^ + | Separation failure: argument of type (f: test2.File^{io3}) ->{io3} Unit + | to method withFile: [T](io2: test2.IO^)(op: (f: test2.File^{io2}) => T): T + | corresponds to capture-polymorphic formal parameter op of type (f: test2.File^{io3}) => Unit + | and captures {io3}, but this capability is also passed separately + | in the first argument with type (io3 : test2.IO^). + | + | Capture set of first argument : {io3} + | Hidden set of current argument : {io3} + | Footprint of first argument : {io3} + | Hidden footprint of current argument : {io3} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {io3} +35 | val o = Service(io3) +36 | o.file = f // this is a bit dubious. It's legal since we treat class refinements +37 | // as capture set variables that can be made to include refs coming from outside. +38 | o.log diff --git a/tests/pos-custom-args/captures/filevar-expanded.scala b/tests/neg-custom-args/captures/filevar-expanded.scala similarity index 90% rename from tests/pos-custom-args/captures/filevar-expanded.scala rename to tests/neg-custom-args/captures/filevar-expanded.scala index 58e7a0e67e0a..c42f9478256f 100644 --- a/tests/pos-custom-args/captures/filevar-expanded.scala +++ b/tests/neg-custom-args/captures/filevar-expanded.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import language.experimental.modularity import compiletime.uninitialized - +import language.future // sepchecks on object test1: class File: def write(x: String): Unit = ??? @@ -31,7 +31,7 @@ object test2: op(new File) def test(io3: IO^) = - withFile(io3): f => + withFile(io3): f => // error: separation failure val o = Service(io3) o.file = f // this is a bit dubious. It's legal since we treat class refinements // as capture set variables that can be made to include refs coming from outside. diff --git a/tests/neg-custom-args/captures/filevar.check b/tests/neg-custom-args/captures/filevar.check new file mode 100644 index 000000000000..22efd36053b4 --- /dev/null +++ b/tests/neg-custom-args/captures/filevar.check @@ -0,0 +1,9 @@ +-- Error: tests/neg-custom-args/captures/filevar.scala:8:6 ------------------------------------------------------------- +8 | var file: File^ = uninitialized // error, was OK under unsealed + | ^ + | Mutable variable file cannot have type File^ since + | that type captures the root capability `cap`. +-- Warning: tests/neg-custom-args/captures/filevar.scala:11:55 --------------------------------------------------------- +11 |def withFile[T](op: (l: caps.Capability) ?-> (f: File^{l}) => T): T = + | ^ + | redundant capture: File already accounts for l.type diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index a8925b117611..78219e0316ee 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -3,3 +3,10 @@ | ^^^ | Type variable T of method usingLogger cannot be instantiated to x.T since | the part () => Logger^ of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i19330.scala:22:22 --------------------------------------- +22 | val bad: bar.T = foo(bar) // error + | ^^^^^^^^ + | Found: () => Logger^ + | Required: () ->{fresh} (ex$9: caps.Exists) -> Logger^{ex$9} + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index 715b670860cd..23fcfa0ffc4f 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -1,7 +1,7 @@ - - +import language.`3.7` // sepchecks on import language.experimental.captureChecking + trait Logger def usingLogger[T](op: Logger^ => T): T = ??? @@ -19,5 +19,5 @@ def foo(x: Foo): x.T = def test(): Unit = val bar = new Bar - val bad: bar.T = foo(bar) + val bad: bar.T = foo(bar) // error val leaked: Logger^ = bad() // leaked scoped capability! diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index f7b45ddf0eaa..109283eae01f 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -2,7 +2,7 @@ 12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: (f: F) ->{files.rd*} box Logger{val f²: File^?}^? - | Required: (f: box F^{files.rd*}) => box Logger{val f²: File^?}^? + | Required: (f: box F^{files.rd*}) ->{fresh} box Logger{val f²: File^?}^? | | where: f is a reference to a value parameter | f² is a value in class Logger diff --git a/tests/neg-custom-args/captures/i21614.scala b/tests/neg-custom-args/captures/i21614.scala index f5bab90f543b..d21fb2f5d3a0 100644 --- a/tests/neg-custom-args/captures/i21614.scala +++ b/tests/neg-custom-args/captures/i21614.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import caps.Capability import caps.use - +import language.`3.7` // sepchecks on trait List[+T]: def map[U](f: T => U): List[U] diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 8683615c07d8..85a76bf5a87c 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -1,28 +1,43 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:19:28 -------------------------------------- -19 | val ref1c: LazyRef[Int] = ref1 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:20:28 -------------------------------------- +20 | val ref1c: LazyRef[Int] = ref1 // error | ^^^^ | Found: (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}) | Required: LazyRef[Int] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:21:35 -------------------------------------- -21 | val ref2c: LazyRef[Int]^{cap2} = ref2 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:22:35 -------------------------------------- +22 | val ref2c: LazyRef[Int]^{cap2} = ref2 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref2*} Int}^{ref2} | Required: LazyRef[Int]^{cap2} | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:23:35 -------------------------------------- -23 | val ref3c: LazyRef[Int]^{ref1} = ref3 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:24:35 -------------------------------------- +24 | val ref3c: LazyRef[Int]^{ref1} = ref3 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref3*} Int}^{ref3} | Required: LazyRef[Int]^{ref1} | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:25:35 -------------------------------------- -25 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:26:35 -------------------------------------- +26 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref4*} Int}^{ref4} | Required: LazyRef[Int]^{cap1} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazyref.scala:25:55 ----------------------------------------------------------- +25 | val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure + | ^ + |Separation failure: argument of type (x: Int) ->{cap2} Int + |to method map: [U](f: T => U): LazyRef[U]^{f, LazyRef.this} + |corresponds to capture-polymorphic formal parameter f of type Int => Int + |and captures {cap2}, but this capability is also passed separately + |in the function prefix with type (LazyRef[Int]{val elem: () ->{ref2*} Int} | (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}))^{ref2}. + | + | Capture set of function prefix : {ref1, ref2} + | Hidden set of current argument : {cap2} + | Footprint of function prefix : {ref1, ref2, cap1, cap2} + | Hidden footprint of current argument : {cap2} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {cap2} diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 99aa10d5d2b2..52e274b65175 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -1,3 +1,4 @@ +import language.`3.7` // sepchecks on class CC type Cap = CC^ @@ -21,5 +22,5 @@ def test(cap1: Cap, cap2: Cap) = val ref2c: LazyRef[Int]^{cap2} = ref2 // error val ref3 = ref1.map(g) val ref3c: LazyRef[Int]^{ref1} = ref3 // error - val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure val ref4c: LazyRef[Int]^{cap1} = ref4 // error diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index b24579b7a69f..0c86213ff118 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -1,5 +1,5 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:11:8 ------------------------------------- -11 | x = q // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:8 ------------------------------------- +12 | x = q // error | ^ | Found: (q : () => Unit) | Required: () ->{p, q²} Unit @@ -8,15 +8,15 @@ | q² is a parameter in method test | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:9 ------------------------------------- -12 | x = (q: Proc) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- +13 | x = (q: Proc) // error | ^^^^^^^ | Found: () => Unit | Required: () ->{p, q} Unit | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- -13 | y = (q: Proc) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:9 ------------------------------------- +14 | y = (q: Proc) // error | ^^^^^^^ | Found: () => Unit | Required: () ->{p} Unit @@ -25,18 +25,18 @@ | cannot be included in capture set {p} of variable y | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:8 ------------------------------------- -14 | y = q // error, was OK under unsealed +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:15:8 ------------------------------------- +15 | y = q // error, was OK under unsealed | ^ | Found: (q : () => Unit) | Required: () ->{p} Unit | | Note that reference (q : () => Unit), defined in method inner - | cannot be included in outer capture set {p} of variable y + | cannot be included in outer capture set {p} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/outer-var.scala:16:57 --------------------------------------------------------- -16 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error, was OK under unsealed +-- Error: tests/neg-custom-args/captures/outer-var.scala:17:57 --------------------------------------------------------- +17 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error, was OK under unsealed | ^^^^^^^^^^ | Type variable A of object ListBuffer cannot be instantiated to box () => Unit since | that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/outer-var.scala b/tests/neg-custom-args/captures/outer-var.scala index f869bfbfc387..4ec19d8f8971 100644 --- a/tests/neg-custom-args/captures/outer-var.scala +++ b/tests/neg-custom-args/captures/outer-var.scala @@ -1,3 +1,4 @@ +import language.`3.7` // sepchecks on class CC type Cap = CC^ diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 7c00fa7299fe..ef755ebfcbd2 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -25,6 +25,20 @@ | ^^^^^^^^^^ | Type variable T of constructor Ref cannot be instantiated to List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:45:35 -------------------------------------- +45 | val next: () => Unit = cur.get.head // error + | ^^^^^^^^^^^^ + | Found: () => Unit + | Required: () ->{fresh} Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:47:20 -------------------------------------- +47 | cur.set(cur.get.tail: List[Proc]) // error + | ^^^^^^^^^^^^ + | Found: List[box () => Unit] + | Required: List[box () ->{fresh} Unit] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/reaches.scala:53:51 ----------------------------------------------------------- 53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error | ^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index a9773b76f445..34f05340a1e7 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,4 +1,4 @@ -import caps.use +import caps.use; import language.`3.7` // sepchecks on class File: def write(): Unit = ??? @@ -42,9 +42,9 @@ def runAll2(xs: List[Proc]): Unit = def runAll3(xs: List[Proc]): Unit = val cur = Ref[List[Proc]](xs) // error while cur.get.nonEmpty do - val next: () => Unit = cur.get.head + val next: () => Unit = cur.get.head // error next() - cur.set(cur.get.tail: List[Proc]) + cur.set(cur.get.tail: List[Proc]) // error class Id[-A, +B >: A](): def apply(a: A): B = a diff --git a/tests/neg-custom-args/captures/sep-compose.check b/tests/neg-custom-args/captures/sep-compose.check new file mode 100644 index 000000000000..7ecab087904e --- /dev/null +++ b/tests/neg-custom-args/captures/sep-compose.check @@ -0,0 +1,120 @@ +-- Error: tests/neg-custom-args/captures/sep-compose.scala:32:10 ------------------------------------------------------- +32 | seq3(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq3: (x: () => Unit)(y: () ->{a, cap} Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () ->{a, cap} Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:33:10 ------------------------------------------------------- +33 | seq4(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq4: (x: () ->{a, cap} Unit)(y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:34:10 ------------------------------------------------------- +34 | seq5(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq5: (x: () => Unit)(y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:35:10 ------------------------------------------------------- +35 | seq6(f, f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq6: (x: () => Unit, y: () ->{a, cap} Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () ->{a, cap} Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:36:10 ------------------------------------------------------- +36 | seq7(f, f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq7: (x: () ->{a, cap} Unit, y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:37:7 -------------------------------------------------------- +37 | seq8(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the second argument with type (f : () ->{a} Unit). + | + | Capture set of second argument : {f} + | Hidden set of current argument : {f} + | Footprint of second argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:40:5 -------------------------------------------------------- +40 | p1(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method apply: (v1: T1): R + | corresponds to capture-polymorphic formal parameter x$0 of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the function prefix with type (p1 : (x$0: () => Unit) ->{f} Unit). + | + | Capture set of function prefix : {p1} + | Hidden set of current argument : {f} + | Footprint of function prefix : {p1, f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:41:38 ------------------------------------------------------- +41 | val p8 = (x: () ->{a} Unit) => seq8(f)(x) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and captures {a, io}, but these capabilities are also passed separately + | in the second argument with type (x : () ->{a} Unit). + | + | Capture set of second argument : {x} + | Hidden set of current argument : {f} + | Footprint of second argument : {x, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {a, io} diff --git a/tests/neg-custom-args/captures/sep-compose.scala b/tests/neg-custom-args/captures/sep-compose.scala new file mode 100644 index 000000000000..268076cd40aa --- /dev/null +++ b/tests/neg-custom-args/captures/sep-compose.scala @@ -0,0 +1,45 @@ +import caps.cap +import language.future // sepchecks on +def seq1(x: () => Unit, y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq2(x: () => Unit)(y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq5(x: () ->{cap} Unit)(y: () => Unit): Unit = + x(); y() + +def test(io: Object^, a: Object^{io}): Unit = + + def seq3(x: () => Unit)(y: () ->{a, cap} Unit): Unit = + x(); y() + + def seq4(x: () ->{a, cap} Unit)(y: () => Unit): Unit = + x(); y() + + def seq6(x: () => Unit, y: () ->{a, cap} Unit): Unit = + x(); y() + + def seq7(x: () ->{a, cap} Unit, y: () => Unit): Unit = + x(); y() + + def seq8(x: () => Unit)(y: () ->{a} Unit): Unit = + x(); y() + + val f = () => println(a) + seq1(f, f) // ok + seq2(f)(f) // ok + seq3(f)(f) // error + seq4(f)(f) // error + seq5(f)(f) // error + seq6(f, f) // error + seq7(f, f) // error + seq8(f)(f) // error + + val p1 = (x: () => Unit) => seq1(f, x) + p1(f) // error + val p8 = (x: () ->{a} Unit) => seq8(f)(x) // error + p8(f) + + + diff --git a/tests/pos-custom-args/captures/readOnly.scala b/tests/neg-custom-args/captures/sepchecks.scala similarity index 66% rename from tests/pos-custom-args/captures/readOnly.scala rename to tests/neg-custom-args/captures/sepchecks.scala index a550010360a3..ceb6ce7b30bb 100644 --- a/tests/pos-custom-args/captures/readOnly.scala +++ b/tests/neg-custom-args/captures/sepchecks.scala @@ -1,5 +1,6 @@ import caps.Mutable import caps.cap +import language.future // sepchecks on trait Rdr[T]: def get: T @@ -9,7 +10,7 @@ class Ref[T](init: T) extends Rdr[T], Mutable: def get: T = current mut def put(x: T): Unit = current = x -def Test(c: Object^) = +def Test(c: Object^): Unit = val a: Ref[Int]^ = Ref(1) val b: Ref[Int]^ = Ref(2) def aa = a @@ -29,6 +30,8 @@ def Test(c: Object^) = setMax2(aa, aa, b) setMax2(a, aa, b) + setMax2(a, b, b) // error + setMax2(b, b, b) // error abstract class IMatrix: def apply(i: Int, j: Int): Double @@ -38,9 +41,22 @@ def Test(c: Object^) = def apply(i: Int, j: Int): Double = arr(i)(j) mut def update(i: Int, j: Int, x: Double): Unit = arr(i)(j) = x - def mul(x: IMatrix^{cap.rd}, y: IMatrix^{cap.rd}, z: Matrix^) = ??? + def mul(x: IMatrix^{cap.rd}, y: IMatrix^{cap.rd}, z: Matrix^): Matrix^ = ??? val m1 = Matrix(10, 10) val m2 = Matrix(10, 10) - mul(m1, m2, m2) // will fail separation checking + mul(m1, m2, m2) // error: will fail separation checking mul(m1, m1, m2) // ok + + def move(get: () => Int, set: Int => Unit) = + set(get()) + + val geta = () => a.get + + def get2(x: () => Int, y: () => Int): (Int, Int) = + (x(), y()) + + move(geta, b.put(_)) // ok + move(geta, a.put(_)) // error + get2(geta, geta) // ok + get2(geta, () => a.get) // ok diff --git a/tests/neg-custom-args/captures/unsound-reach-2.scala b/tests/neg-custom-args/captures/unsound-reach-2.scala index c7dfa117a2fe..90dd3824099f 100644 --- a/tests/neg-custom-args/captures/unsound-reach-2.scala +++ b/tests/neg-custom-args/captures/unsound-reach-2.scala @@ -1,4 +1,4 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait Consumer[-T]: def apply(x: T): Unit @@ -13,7 +13,7 @@ class Bar extends Foo[File^]: // error def use(x: File^)(op: Consumer[File^]): Unit = op.apply(x) def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala index c5cdfca9d87a..0992dffb63ff 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -1,6 +1,6 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait File: def close(): Unit @@ -12,7 +12,7 @@ class Bar extends Foo[File^]: // error def use(x: File^): File^ = x def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index ca95bf42ba59..2d00eb0364e0 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -3,6 +3,13 @@ | ^^^^^^^^^^ | Type variable X of trait Foo cannot be instantiated to File^ since | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-4.scala:17:29 ------------------------------ +17 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). + | ^^^^^^^ + | Found: Bar^? + | Required: Foo[box File^] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:22:22 --------------------------------------------------- 22 | escaped = boom.use(f) // error | ^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index 88fbc2f5c1de..bba09c0286e3 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -1,6 +1,6 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait File: def close(): Unit @@ -14,7 +14,7 @@ class Bar extends Foo[File^]: // error def use(x: F): File^ = x def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 69794f569edb..17d4a4420833 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -8,6 +8,13 @@ | ^ | Type variable X of constructor Foo2 cannot be instantiated to box File^ since | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach.scala:18:31 -------------------------------- +18 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). + | ^^^^^^^ + | Found: Bar^? + | Required: Foo[box File^] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach.scala:23:21 ----------------------------------------------------- 23 | boom.use(f): (f1: File^{backdoor*}) => // error | ^ diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index 3fb666c7c1fc..fc8e2328ceb8 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -1,4 +1,4 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait File: def close(): Unit @@ -15,7 +15,7 @@ class Bar2 extends Foo2[File^]: // error def use(x: File^)(op: File^ => Unit): Unit = op(x) // OK using sealed checking def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/update-call.scala b/tests/neg-custom-args/captures/update-call.scala new file mode 100644 index 000000000000..848e4d880223 --- /dev/null +++ b/tests/neg-custom-args/captures/update-call.scala @@ -0,0 +1,19 @@ +import caps.Mutable + +trait IterableOnce[T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ??? + override mut def toString = ??? // error + +trait Iterable[T] extends IterableOnce[T]: + def iterator: Iterator[T] = ??? + def foreach(op: T => Unit) = iterator.foreach(op) + +trait BadIterator[T] extends Iterator[T]: + override mut def hasNext: Boolean // error diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index db5c8083e3b7..4fe4163aa433 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,10 +1,11 @@ -- Error: tests/neg-custom-args/captures/vars.scala:24:14 -------------------------------------------------------------- 24 | a = x => g(x) // error | ^^^^ - | reference (cap3 : CC^) is not included in the allowed capture set {cap1} of variable a + | reference (cap3 : CC^) is not included in the allowed capture set {cap1} + | of an enclosing function literal with expected type (x$0: String) ->{cap1} String | | Note that reference (cap3 : CC^), defined in method scope - | cannot be included in outer capture set {cap1} of variable a + | cannot be included in outer capture set {cap1} -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:8 ------------------------------------------ 25 | a = g // error | ^ @@ -12,7 +13,7 @@ | Required: (x$0: String) ->{cap1} String | | Note that reference (cap3 : CC^), defined in method scope - | cannot be included in outer capture set {cap1} of variable a + | cannot be included in outer capture set {cap1} | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:27:12 ----------------------------------------- diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index eb9719cd2adf..fc0de7354dd3 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -1,4 +1,4 @@ - +import language.`3.7` // sepchecks on class CC type Cap = CC^ diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala index 20282d5813f9..436132280d40 100644 --- a/tests/pos-custom-args/captures/boxmap-paper.scala +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -1,3 +1,4 @@ +import caps.cap type Cell_orig[+T] = [K] -> (T => K) -> K @@ -18,13 +19,13 @@ def map[A, B](c: Cell[A])(f: A => B): Cell[B] def pureMap[A, B](c: Cell[A])(f: A -> B): Cell[B] = c[Cell[B]]((x: A) => cell(f(x))) -def lazyMap[A, B](c: Cell[A])(f: A => B): () ->{f} Cell[B] +def lazyMap[A, B](c: Cell[A])(f: A ->{cap.rd} B): () ->{f} Cell[B] = () => c[Cell[B]]((x: A) => cell(f(x))) trait IO: def print(s: String): Unit -def test(io: IO^) = +def test(io: IO^{cap.rd}) = val loggedOne: () ->{io} Int = () => { io.print("1"); 1 } diff --git a/tests/pos-custom-args/captures/cc-dep-param.scala b/tests/pos-custom-args/captures/cc-dep-param.scala index 1440cd4d7d40..5fd634de9040 100644 --- a/tests/pos-custom-args/captures/cc-dep-param.scala +++ b/tests/pos-custom-args/captures/cc-dep-param.scala @@ -1,8 +1,9 @@ import language.experimental.captureChecking +import caps.cap trait Foo[T] def test(): Unit = - val a: Foo[Int]^ = ??? + val a: Foo[Int]^{cap.rd} = ??? val useA: () ->{a} Unit = ??? def foo[X](x: Foo[X]^, op: () ->{x} Unit): Unit = ??? foo(a, useA) diff --git a/tests/pos-custom-args/captures/foreach2.scala b/tests/pos-custom-args/captures/foreach2.scala new file mode 100644 index 000000000000..318bcb9cddfc --- /dev/null +++ b/tests/pos-custom-args/captures/foreach2.scala @@ -0,0 +1,7 @@ +import annotation.unchecked.uncheckedCaptures + +class ArrayBuffer[T]: + def foreach(op: T => Unit): Unit = ??? +def test = + val tasks = new ArrayBuffer[(() => Unit) @uncheckedCaptures] + val _: Unit = tasks.foreach(((task: () => Unit) => task())) diff --git a/tests/pos-custom-args/captures/nested-classes-2.scala b/tests/pos-custom-args/captures/nested-classes-2.scala index 744635ee949b..7290ed4a12ea 100644 --- a/tests/pos-custom-args/captures/nested-classes-2.scala +++ b/tests/pos-custom-args/captures/nested-classes-2.scala @@ -1,21 +1,7 @@ - -def f(x: (() => Unit)): (() => Unit) => (() => Unit) = - def g(y: (() => Unit)): (() => Unit) = x - g - -def test1(x: (() => Unit)): Unit = - def test2(y: (() => Unit)) = - val a: (() => Unit) => (() => Unit) = f(y) - a(x) // OK, but should be error - test2(() => ()) - def test2(x1: (() => Unit), x2: (() => Unit) => Unit) = class C1(x1: (() => Unit), xx2: (() => Unit) => Unit): - def c2(y1: (() => Unit), y2: (() => Unit) => Unit): C2^ = C2(y1, y2) - class C2(y1: (() => Unit), y2: (() => Unit) => Unit): - val a: (() => Unit) => (() => Unit) = f(y1) - a(x1) //OK, but should be error - C2(() => (), x => ()) + def c2(y1: (() => Unit), y2: (() => Unit) => Unit): C2^ = ??? + class C2(y1: (() => Unit), y2: (() => Unit) => Unit) def test3(y1: (() => Unit), y2: (() => Unit) => Unit) = val cc1: C1^{y1, y2} = C1(y1, y2) diff --git a/tests/pos-custom-args/captures/sep-compose.scala b/tests/pos-custom-args/captures/sep-compose.scala new file mode 100644 index 000000000000..3f6ef2968a6e --- /dev/null +++ b/tests/pos-custom-args/captures/sep-compose.scala @@ -0,0 +1,21 @@ +import caps.cap +import language.future // sepchecks on + +def seq1(x: () => Unit, y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq2(x: () => Unit)(y: () ->{x, cap} Unit): Unit = + x(); y() + +def test(io: Object^, a: Object^{io}): Unit = + val f = () => println(a) + val g = () => println(a) + seq1(f, f) + seq2(f)(f) + seq1(g, g) + seq2(g)(g) + + seq1(f, g) + seq2(f)(g) + seq1(g, f) + seq2(g)(f) \ No newline at end of file diff --git a/tests/pos-custom-args/captures/sep-eq.scala b/tests/pos-custom-args/captures/sep-eq.scala new file mode 100644 index 000000000000..836633feee9e --- /dev/null +++ b/tests/pos-custom-args/captures/sep-eq.scala @@ -0,0 +1,20 @@ +import caps.Mutable +import caps.cap +import language.future // sepchecks on + +extension (x: Object^) + infix def eql (y: Object^{x, cap}): Boolean = x eq y + +def eql1(x: Object^, y: Object^{x, cap}): Boolean = x eql y +def eql2(x: Object^)(y: Object^{x, cap}): Boolean = x eql y + +class LLI extends Object: + this: LLI^ => + + val f: Object^ = ??? + + def foo = + def these = f + val eq0 = these eql these + val eq1 = eql2(f)(f) + val eq2 = eql2(these)(these) diff --git a/tests/pos-custom-args/captures/simple-apply.scala b/tests/pos-custom-args/captures/simple-apply.scala new file mode 100644 index 000000000000..1e2a6715dd79 --- /dev/null +++ b/tests/pos-custom-args/captures/simple-apply.scala @@ -0,0 +1,6 @@ +object Test: + + def foo(x: Object^, ys: List[Object^]) = ??? + def test(io: Object^, async: Object^): Unit = + val v: Object^{io} = ??? + foo(v, List(async)) diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala new file mode 100644 index 000000000000..dd6417042339 --- /dev/null +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -0,0 +1,15 @@ +def Test(c: Object^, f: Object^ => Object^) = + def cc: Object^ = c + val x1 = + { f(cc) } + val x2 = + f(cc) + val x3: Object^ = + f(cc) + val x4: Object^ = + { f(cc) } + + + + + diff --git a/tests/pos-special/stdlib/Test2.scala b/tests/pos-special/stdlib/Test2.scala index cab9440c17db..e0d9a1491516 100644 --- a/tests/pos-special/stdlib/Test2.scala +++ b/tests/pos-special/stdlib/Test2.scala @@ -2,6 +2,7 @@ import scala.reflect.ClassTag import language.experimental.captureChecking import collection.{View, Seq} import collection.mutable.{ArrayBuffer, ListBuffer} +import caps.unsafe.unsafeAssumeSeparate object Test { @@ -87,7 +88,7 @@ object Test { val ys9: Iterator[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: Iterator[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: Iterator[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: Iterator[Int]^{xs12} = xs12 @@ -95,7 +96,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ ("a" :: Nil) val ys14: Iterator[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: Iterator[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) @@ -141,7 +142,7 @@ object Test { val ys9: View[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: View[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: View[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: View[Int]^{xs12} = xs12 @@ -149,7 +150,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ ("a" :: Nil) val ys14: View[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: View[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index 5443758afa72..c22e1308db6d 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -5,6 +5,8 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag import annotation.unchecked.{uncheckedVariance, uncheckedCaptures} import annotation.tailrec +import caps.cap +import language.`3.7` // sepchecks on /** A strawman architecture for new collections. It contains some * example collection classes and methods with the intent to expose @@ -29,7 +31,7 @@ object CollectionStrawMan5 { /** Base trait for instances that can construct a collection from an iterable */ trait FromIterable { type C[X] <: Iterable[X]^ - def fromIterable[B](it: Iterable[B]^): C[B]^{it} + def fromIterable[B](it: Iterable[B]^{this, cap}): C[B]^{it} } type FromIterableOf[+CC[X] <: Iterable[X]^] = FromIterable { @@ -60,12 +62,12 @@ object CollectionStrawMan5 { trait SeqFactory extends IterableFactory { type C[X] <: Seq[X] - def fromIterable[B](it: Iterable[B]^): C[B] + def fromIterable[B](it: Iterable[B]^{this, cap}): C[B] } /** Base trait for strict collections */ trait Buildable[+A] extends Iterable[A] { - protected[this] def newBuilder: Builder[A, Repr] @uncheckedVariance + protected def newBuilder: Builder[A, Repr] @uncheckedVariance override def partition(p: A => Boolean): (Repr, Repr) = { val l, r = newBuilder iterator.foreach(x => (if (p(x)) l else r) += x) @@ -105,7 +107,7 @@ object CollectionStrawMan5 { with IterablePolyTransforms[A] with IterableMonoTransforms[A] { // sound bcs of VarianceNote type Repr = C[A] @uncheckedVariance - protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr @uncheckedVariance ^{coll} = + protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ {this, cap}): Repr @uncheckedVariance ^{coll} = fromIterable(coll) } @@ -115,7 +117,7 @@ object CollectionStrawMan5 { this: SeqLike[A] => type C[X] <: Seq[X] def fromIterable[B](coll: Iterable[B]^): C[B] - override protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr = + override protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ ): Repr = fromIterable(coll) trait IterableOps[+A] extends Any { @@ -134,7 +136,7 @@ object CollectionStrawMan5 { this: IterableMonoTransforms[A]^ => type Repr protected def coll: Iterable[A]^{this} - protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr^{coll} + protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ {this, cap}): Repr^{coll} def filter(p: A => Boolean): Repr^{this, p} = fromLikeIterable(View.Filter(coll, p)) def partition(p: A => Boolean): (Repr^{this, p}, Repr^{this, p}) = { @@ -153,7 +155,7 @@ object CollectionStrawMan5 { this: IterablePolyTransforms[A]^ => type C[A] protected def coll: Iterable[A]^{this} - def fromIterable[B](coll: Iterable[B]^): C[B]^{coll} + def fromIterable[B](coll: Iterable[B]^{this, cap}): C[B]^{coll} def map[B](f: A => B): C[B]^{this, f} = fromIterable(View.Map(coll, f)) def flatMap[B](f: A => IterableOnce[B]^): C[B]^{this, f} = fromIterable(View.FlatMap(coll, f)) def ++[B >: A](xs: IterableOnce[B]^): C[B]^{this, xs} = fromIterable(View.Concat(coll, xs)) @@ -169,7 +171,7 @@ object CollectionStrawMan5 { while (it.hasNext) xs = new Cons(it.next(), xs) fromLikeIterable(xs) - override protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr + override protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr override def filter(p: A => Boolean): Repr = fromLikeIterable(View.Filter(coll, p)) @@ -204,7 +206,7 @@ object CollectionStrawMan5 { def head: A def tail: List[A] def iterator = new Iterator[A] { - private[this] var current = self + private var current = self def hasNext = !current.isEmpty def next() = { val r = current.head; current = current.tail; r } } @@ -215,7 +217,7 @@ object CollectionStrawMan5 { } def length: Int = if (isEmpty) 0 else 1 + tail.length - protected[this] def newBuilder = new ListBuffer[A @uncheckedVariance @uncheckedCaptures] + protected def newBuilder = new ListBuffer[A @uncheckedVariance @uncheckedCaptures] def ++:[B >: A](prefix: List[B]): List[B] = if (prefix.isEmpty) this else Cons(prefix.head, prefix.tail ++: this) @@ -407,7 +409,7 @@ object CollectionStrawMan5 { this: View[A]^ => type C[X] = View[X]^{this} override def view: this.type = this - override def fromIterable[B](c: Iterable[B]^): View[B]^{this, c} = { + override def fromIterable[B](c: Iterable[B]^{this, cap}): View[B]^{this, c} = { c match { case c: View[B] => c case _ => View.fromIterator(c.iterator) diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index f6f47b536541..2b3b27c94243 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -1,5 +1,7 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag +import caps.unsafe.unsafeAssumeSeparate +import language.`3.7` // sepchecks on object Test { import colltest5.strawman.collections.* @@ -89,7 +91,7 @@ object Test { val ys9: View[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: View[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: View[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: View[Int]^{xs12} = xs12 @@ -97,7 +99,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ Cons("a", Nil) val ys14: View[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: View[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) From b0c9b3d60cce68fc41b4360ba7197cee05fd09d9 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 12 Jan 2025 19:21:15 +0100 Subject: [PATCH 06/93] Separation checking for blocks Check that a capability that gets hidden in the (result-)type of some definition is not used afterwards in the same or a nested scope. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 78 +++++---- .../src/dotty/tools/dotc/cc/SepCheck.scala | 159 +++++++++++++----- project/Build.scala | 2 +- .../immutable/LazyListIterable.scala | 3 +- tests/neg-custom-args/captures/capt1.check | 68 +++++--- tests/neg-custom-args/captures/capt1.scala | 14 +- .../captures/cc-ex-conformance.scala | 3 +- tests/neg-custom-args/captures/i15772.check | 24 +-- tests/neg-custom-args/captures/i15772.scala | 2 + .../captures/sep-compose.check | 4 +- tests/neg-custom-args/captures/sep-use.check | 24 +++ tests/neg-custom-args/captures/sep-use.scala | 27 +++ tests/neg-custom-args/captures/sep-use2.scala | 28 +++ tests/pos-custom-args/captures/capt1.scala | 12 +- tests/pos-custom-args/captures/skolems2.scala | 2 + 15 files changed, 328 insertions(+), 122 deletions(-) create mode 100644 tests/neg-custom-args/captures/sep-use.check create mode 100644 tests/neg-custom-args/captures/sep-use.scala create mode 100644 tests/neg-custom-args/captures/sep-use2.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index d494bc8d9e22..c5de4e97807e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -18,7 +18,7 @@ import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* import scala.collection.mutable -import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult, VarState} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} @@ -253,6 +253,10 @@ object CheckCaptures: * the type of the formal paremeter corresponding to the argument. */ def formalType: Type + + /** The "use set", i.e. the capture set marked as free at this node. */ + def markedFree: CaptureSet + end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -298,9 +302,12 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val sepCheckFormals = util.EqHashMap[Tree, Type]() + private val usedSet = util.EqHashMap[Tree, CaptureSet]() + extension [T <: Tree](tree: T) def needsSepCheck: Boolean = sepCheckFormals.contains(tree) def formalType: Type = sepCheckFormals.getOrElse(tree, NoType) + def markedFree = usedSet.getOrElse(tree, CaptureSet.empty) /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. @@ -404,17 +411,17 @@ class CheckCaptures extends Recheck, SymTransformer: /** Include `sym` in the capture sets of all enclosing environments nested in the * the environment in which `sym` is defined. */ - def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = - markFree(sym, sym.termRef, pos) + def markFree(sym: Symbol, tree: Tree)(using Context): Unit = + markFree(sym, sym.termRef, tree) - def markFree(sym: Symbol, ref: CaptureRef, pos: SrcPos)(using Context): Unit = - if sym.exists && ref.isTracked then markFree(ref.captureSet, pos) + def markFree(sym: Symbol, ref: CaptureRef, tree: Tree)(using Context): Unit = + if sym.exists && ref.isTracked then markFree(ref.captureSet, tree) /** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing * environments. At each stage, only include references from `cs` that are outside * the environment's owner */ - def markFree(cs: CaptureSet, pos: SrcPos)(using Context): Unit = + def markFree(cs: CaptureSet, tree: Tree)(using Context): Unit = // A captured reference with the symbol `sym` is visible from the environment // if `sym` is not defined inside the owner of the environment. inline def isVisibleFromEnv(sym: Symbol, env: Env) = @@ -436,7 +443,7 @@ class CheckCaptures extends Recheck, SymTransformer: val what = if ref.isType then "Capture set parameter" else "Local reach capability" report.error( em"""$what $c leaks into capture scope of ${env.ownerString}. - |To allow this, the ${ref.symbol} should be declared with a @use annotation""", pos) + |To allow this, the ${ref.symbol} should be declared with a @use annotation""", tree.srcPos) case _ => /** Avoid locally defined capability by charging the underlying type @@ -456,7 +463,7 @@ class CheckCaptures extends Recheck, SymTransformer: CaptureSet.ofType(c.widen, followResult = false) capt.println(i"Widen reach $c to $underlying in ${env.owner}") underlying.disallowRootCapability: () => - report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", pos) + report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", tree.srcPos) recur(underlying, env, lastEnv) /** Avoid locally defined capability if it is a reach capability or capture set @@ -479,7 +486,7 @@ class CheckCaptures extends Recheck, SymTransformer: val underlying = CaptureSet.ofTypeDeeply(c1.widen) capt.println(i"Widen reach $c to $underlying in ${env.owner}") underlying.disallowRootCapability: () => - report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) + report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", tree.srcPos) recur(underlying, env, null) case c: TypeRef if c.isParamPath => checkUseDeclared(c, env, null) @@ -496,7 +503,7 @@ class CheckCaptures extends Recheck, SymTransformer: then avoidLocalCapability(c, env, lastEnv) else avoidLocalReachCapability(c, env) isVisible - checkSubset(included, env.captured, pos, provenance(env)) + checkSubset(included, env.captured, tree.srcPos, provenance(env)) capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") if !isOfNestedMethod(env) then recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner), env) @@ -504,14 +511,15 @@ class CheckCaptures extends Recheck, SymTransformer: // will be charged when that method is called. recur(cs, curEnv, null) + usedSet(tree) = tree.markedFree ++ cs end markFree /** Include references captured by the called method in the current environment stack */ - def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match + def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => if sym.exists then - if curEnv.isOpen then markFree(capturedVars(sym), pos) + if curEnv.isOpen then markFree(capturedVars(sym), tree) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType @@ -535,23 +543,23 @@ class CheckCaptures extends Recheck, SymTransformer: for case (arg: TypeTree, pname) <- args.lazyZip(paramNames) do def where = if sym.exists then i" in an argument of $sym" else "" - val (addendum, pos) = + val (addendum, errTree) = if arg.isInferred - then ("\nThis is often caused by a local capability$where\nleaking as part of its result.", fn.srcPos) - else if arg.span.exists then ("", arg.srcPos) - else ("", fn.srcPos) + then ("\nThis is often caused by a local capability$where\nleaking as part of its result.", fn) + else if arg.span.exists then ("", arg) + else ("", fn) disallowRootCapabilitiesIn(arg.nuType, NoSymbol, - i"Type variable $pname of $sym", "be instantiated to", addendum, pos) + i"Type variable $pname of $sym", "be instantiated to", addendum, errTree.srcPos) val param = fn.symbol.paramNamed(pname) - if param.isUseParam then markFree(arg.nuType.deepCaptureSet, pos) + if param.isUseParam then markFree(arg.nuType.deepCaptureSet, errTree) end disallowCapInTypeArgs override def recheckIdent(tree: Ident, pt: Type)(using Context): Type = val sym = tree.symbol if sym.is(Method) then // If ident refers to a parameterless method, charge its cv to the environment - includeCallCaptures(sym, sym.info, tree.srcPos) + includeCallCaptures(sym, sym.info, tree) else if !sym.isStatic then // Otherwise charge its symbol, but add all selections implied by the e // expected type `pt`. @@ -569,7 +577,7 @@ class CheckCaptures extends Recheck, SymTransformer: var pathRef: CaptureRef = addSelects(sym.termRef, pt) if pathRef.derivesFrom(defn.Caps_Mutable) && pt.isValueType && !pt.isMutableType then pathRef = pathRef.readOnly - markFree(sym, pathRef, tree.srcPos) + markFree(sym, pathRef, tree) super.recheckIdent(tree, pt) /** The expected type for the qualifier of a selection. If the selection @@ -668,7 +676,7 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckFinish(argType, tree, pt) else val res = super.recheckApply(tree, pt) - includeCallCaptures(meth, res, tree.srcPos) + includeCallCaptures(meth, res, tree) res /** Recheck argument, and, if formal parameter carries a `@use`, @@ -681,7 +689,7 @@ class CheckCaptures extends Recheck, SymTransformer: if formal.hasUseAnnot then // The @use annotation is added to `formal` by `prepareFunction` capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") - markFree(argType.deepCaptureSet, arg.srcPos) + markFree(argType.deepCaptureSet, arg) if formal.containsCap then sepCheckFormals(arg) = freshenedFormal argType @@ -815,7 +823,7 @@ class CheckCaptures extends Recheck, SymTransformer: case fun => fun.symbol disallowCapInTypeArgs(tree.fun, meth, tree.args) val res = Existential.toCap(super.recheckTypeApply(tree, pt)) - includeCallCaptures(tree.symbol, res, tree.srcPos) + includeCallCaptures(tree.symbol, res, tree) checkContains(tree) res end recheckTypeApply @@ -1092,7 +1100,7 @@ class CheckCaptures extends Recheck, SymTransformer: case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot => annot.tree match case Apply(_, cap :: Nil) => - markFree(cap.symbol, tree.srcPos) + markFree(cap.symbol, tree) case _ => case _ => super.recheckTyped(tree) @@ -1147,7 +1155,7 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheck(tree, pt) finally curEnv = saved if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then - markFree(res.boxedCaptureSet, tree.srcPos) + markFree(res.boxedCaptureSet, tree) res end recheck @@ -1214,7 +1222,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type = var expected1 = alignDependentFunction(expected, actual.stripCapturing) val boxErrors = new mutable.ListBuffer[Message] - val actualBoxed = adapt(actual, expected1, tree.srcPos, boxErrors) + val actualBoxed = adapt(actual, expected1, tree, boxErrors) //println(i"check conforms $actualBoxed <<< $expected1") if actualBoxed eq actual then @@ -1334,7 +1342,7 @@ class CheckCaptures extends Recheck, SymTransformer: * * @param alwaysConst always make capture set variables constant after adaptation */ - def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = + def adaptBoxed(actual: Type, expected: Type, tree: Tree, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = def recur(actual: Type, expected: Type, covariant: Boolean): Type = @@ -1401,7 +1409,7 @@ class CheckCaptures extends Recheck, SymTransformer: if !leaked.subCaptures(cs).isOK then report.error( em"""$expected cannot be box-converted to ${actual.capturing(leaked)} - |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos) + |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", tree.srcPos) cs def adaptedType(resultBoxed: Boolean) = @@ -1433,11 +1441,11 @@ class CheckCaptures extends Recheck, SymTransformer: return actual // Disallow future addition of `cap` to `criticalSet`. criticalSet.disallowRootCapability: () => - report.error(msg, pos) + report.error(msg, tree.srcPos) if !insertBox then // we are unboxing //debugShowEnvs() - markFree(criticalSet, pos) + markFree(criticalSet, tree) end if // Compute the adapted type. @@ -1497,14 +1505,14 @@ class CheckCaptures extends Recheck, SymTransformer: * - narrow nested captures of `x`'s underlying type to `{x*}` * - do box adaptation */ - def adapt(actual: Type, expected: Type, pos: SrcPos, boxErrors: BoxErrors)(using Context): Type = + def adapt(actual: Type, expected: Type, tree: Tree, boxErrors: BoxErrors)(using Context): Type = if expected == LhsProto || expected.isSingleton && actual.isSingleton then actual else val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) val improvedRO = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( - improvedRO.withReachCaptures(actual), expected, pos, + improvedRO.withReachCaptures(actual), expected, tree, covariant = true, alwaysConst = false, boxErrors) if adapted eq improvedVAR // no .rd improvement, no box-adaptation then actual // might as well use actual instead of improved widened @@ -1519,19 +1527,19 @@ class CheckCaptures extends Recheck, SymTransformer: * But maybe we can then elide the check during the RefChecks phase under captureChecking? */ def checkOverrides = new TreeTraverser: - class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, srcPos: SrcPos)(using Context) extends OverridingPairsChecker(clazz, self): + class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, tree: Tree)(using Context) extends OverridingPairsChecker(clazz, self): /** Check subtype with box adaptation. * This function is passed to RefChecks to check the compatibility of overriding pairs. * @param sym symbol of the field definition that is being checked */ override def checkSubType(actual: Type, expected: Type)(using Context): Boolean = - val expected1 = alignDependentFunction(addOuterRefs(expected, actual, srcPos), actual.stripCapturing) + val expected1 = alignDependentFunction(addOuterRefs(expected, actual, tree.srcPos), actual.stripCapturing) val actual1 = val saved = curEnv try curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) val adapted = - adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true, null) + adaptBoxed(actual, expected1, tree, covariant = true, alwaysConst = true, null) actual match case _: MethodType => // We remove the capture set resulted from box adaptation for method types, diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 9f5e8187d1d0..ecdb2cc93a82 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -5,16 +5,32 @@ import ast.tpd import collection.mutable import core.* -import Symbols.*, Types.* +import Symbols.*, Types.*, Flags.* import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* -import CaptureSet.{Refs, emptySet} +import CaptureSet.{Refs, emptySet, HiddenSet} import config.Printers.capt import StdNames.nme +import util.{SimpleIdentitySet, EqHashMap} class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import tpd.* import checker.* + /** The set of capabilities that are hidden by a polymorphic result type + * of some previous definition. + */ + private var defsShadow: Refs = SimpleIdentitySet.empty + + /** A map from definitions to their internal result types. + * Populated during separation checking traversal. + */ + private val resultType = EqHashMap[Symbol, Type]() + + /** The previous val or def definitions encountered during separation checking. + * These all enclose and precede the current traversal node. + */ + private var previousDefs: List[mutable.ListBuffer[ValOrDefDef]] = Nil + extension (refs: Refs) private def footprint(using Context): Refs = def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match @@ -34,38 +50,39 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: ref.isExclusive && refs2.exists(_.stripReadOnly eq ref) common(refs, other) ++ common(other, refs) - private def hidden(refs: Refs)(using Context): Refs = - val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + private def hidden(using Context): Refs = + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet - def hiddenByElem(elem: CaptureRef): Refs = - if seen.add(elem) then elem match - case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) - case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) - case _ => emptySet - else emptySet + def hiddenByElem(elem: CaptureRef): Refs = + if seen.add(elem) then elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) + case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) + case _ => emptySet + else emptySet - def recur(cs: Refs): Refs = - (emptySet /: cs): (elems, elem) => - elems ++ hiddenByElem(elem) + def recur(cs: Refs): Refs = + (emptySet /: cs): (elems, elem) => + elems ++ hiddenByElem(elem) - recur(refs) - end hidden + recur(refs) + end hidden + end extension /** The captures of an argument or prefix widened to the formal parameter, if * the latter contains a cap. */ private def formalCaptures(arg: Tree)(using Context): Refs = val argType = arg.formalType.orElse(arg.nuType) - (if arg.nuType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) + (if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) .elems - /** The captures of an argument of prefix. No widening takes place */ - private def actualCaptures(arg: Tree)(using Context): Refs = - val argType = arg.nuType - (if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) + /** The captures of a node */ + private def captures(tree: Tree)(using Context): Refs = + val tpe = tree.nuType + (if tree.formalType.hasUseAnnot then tpe.deepCaptureSet else tpe.captureSet) .elems - private def sepError(fn: Tree, args: List[Tree], argIdx: Int, + private def sepApplyError(fn: Tree, args: List[Tree], argIdx: Int, overlap: Refs, hiddenInArg: Refs, footprints: List[(Refs, Int)], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = val arg = args(argIdx) @@ -78,9 +95,15 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case Some(pname) => i"$pname " case _ => "" def whatStr = if overlap.size == 1 then "this capability is" else "these capabilities are" + def qualifier = methPart(fn) match + case Select(qual, _) => qual + case _ => EmptyTree + def isShowableMethod = fn.symbol.exists && !defn.isFunctionSymbol(fn.symbol.maybeOwner) + def funType = + if fn.symbol.exists && !qualifier.isEmpty then qualifier.nuType else fn.nuType def funStr = - if fn.symbol.exists then i"${fn.symbol}: ${fn.symbol.info}" - else i"a function of type ${fn.nuType.widen}" + if isShowableMethod then i"${fn.symbol}: ${fn.symbol.info}" + else i"a function of type ${funType.widen}" val clashIdx = footprints .collect: case (fp, idx) if !hiddenInArg.overlapWith(fp).isEmpty => idx @@ -92,21 +115,23 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case 3 => "third argument " case n => s"${n}th argument " def clashTree = - if clashIdx == 0 then methPart(fn).asInstanceOf[Select].qualifier + if clashIdx == 0 then qualifier else args(clashIdx - 1) - def clashType = clashTree.nuType - def clashCaptures = actualCaptures(clashTree) - def hiddenCaptures = hidden(formalCaptures(arg)) + def clashTypeStr = + if clashIdx == 0 && !isShowableMethod then "" // we already mentioned the type in `funStr` + else i" with type ${clashTree.nuType}" + def clashCaptures = captures(clashTree) + def hiddenCaptures = formalCaptures(arg).hidden def clashFootprint = clashCaptures.footprint def hiddenFootprint = hiddenCaptures.footprint - def declaredFootprint = deps(arg).map(actualCaptures(_)).foldLeft(emptySet)(_ ++ _).footprint + def declaredFootprint = deps(arg).map(captures(_)).foldLeft(emptySet)(_ ++ _).footprint def footprintOverlap = hiddenFootprint.overlapWith(clashFootprint) -- declaredFootprint report.error( em"""Separation failure: argument of type ${arg.nuType} |to $funStr |corresponds to capture-polymorphic formal parameter ${formalName}of type ${arg.formalType} |and captures ${CaptureSet(overlap)}, but $whatStr also passed separately - |in the ${whereStr.trim} with type $clashType. + |in the ${whereStr.trim}$clashTypeStr. | | Capture set of $whereStr : ${CaptureSet(clashCaptures)} | Hidden set of current argument : ${CaptureSet(hiddenCaptures)} @@ -115,7 +140,28 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: | Declared footprint of current argument: ${CaptureSet(declaredFootprint)} | Undeclared overlap of footprints : ${CaptureSet(footprintOverlap)}""", arg.srcPos) - end sepError + end sepApplyError + + def sepUseError(tree: Tree, used: Refs, globalOverlap: Refs)(using Context): Unit = + val individualChecks = for mdefs <- previousDefs.iterator; mdef <- mdefs.iterator yield + val hiddenByDef = captures(mdef.tpt).hidden + val overlap = defUseOverlap(hiddenByDef, used, tree.symbol) + if !overlap.isEmpty then + def resultStr = if mdef.isInstanceOf[DefDef] then " result" else "" + report.error( + em"""Separation failure: Illegal access to ${CaptureSet(overlap)} which is hidden by the previous definition + |of ${mdef.symbol} with$resultStr type ${mdef.tpt.nuType}. + |This type hides capabilities ${CaptureSet(hiddenByDef)}""", + tree.srcPos) + true + else false + val clashes = individualChecks.filter(identity) + if clashes.hasNext then clashes.next // issues error as a side effect + else report.error( + em"""Separation failure: Illegal access to ${CaptureSet(globalOverlap)} which is hidden by some previous definitions + |No clashing definitions were found. This might point to an internal error.""", + tree.srcPos) + end sepUseError private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = val fnCaptures = methPart(fn) match @@ -128,24 +174,41 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def subtractDeps(elems: Refs, arg: Tree): Refs = deps(arg).foldLeft(elems): (elems, dep) => - elems -- actualCaptures(dep).footprint + elems -- captures(dep).footprint for (arg, idx) <- indexedArgs do if !arg.needsSepCheck then - footprint = footprint ++ subtractDeps(actualCaptures(arg).footprint, arg) + footprint = footprint ++ subtractDeps(captures(arg).footprint, arg) footprints += ((footprint, idx + 1)) for (arg, idx) <- indexedArgs do if arg.needsSepCheck then val ac = formalCaptures(arg) - val hiddenInArg = hidden(ac).footprint + val hiddenInArg = ac.hidden.footprint //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") val overlap = subtractDeps(hiddenInArg.overlapWith(footprint), arg) if !overlap.isEmpty then - sepError(fn, args, idx, overlap, hiddenInArg, footprints.toList, deps) - footprint ++= actualCaptures(arg).footprint + sepApplyError(fn, args, idx, overlap, hiddenInArg, footprints.toList, deps) + footprint ++= captures(arg).footprint footprints += ((footprint, idx + 1)) end checkApply + def defUseOverlap(hiddenByDef: Refs, used: Refs, sym: Symbol)(using Context): Refs = + val overlap = hiddenByDef.overlapWith(used) + resultType.get(sym) match + case Some(tp) if !overlap.isEmpty => + val declared = tp.captureSet.elems + overlap -- declared.footprint -- declared.hidden.footprint + case _ => + overlap + + def checkUse(tree: Tree)(using Context) = + val used = tree.markedFree + if !used.elems.isEmpty then + val usedFootprint = used.elems.footprint + val overlap = defUseOverlap(defsShadow, usedFootprint, tree.symbol) + if !overlap.isEmpty then + sepUseError(tree, usedFootprint, overlap) + private def collectMethodTypes(tp: Type): List[TermLambda] = tp match case tp: MethodType => tp :: collectMethodTypes(tp.resType) case tp: PolyType => collectMethodTypes(tp.resType) @@ -184,13 +247,29 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: checkApply(tree, argss.flatten, dependencies(tree, argss)) def traverse(tree: Tree)(using Context): Unit = + tree match + case tree: Apply if tree.symbol == defn.Caps_unsafeAssumeSeparate => return + case _ => + checkUse(tree) tree match case tree: GenericApply => - if tree.symbol != defn.Caps_unsafeAssumeSeparate then - tree.tpe match - case _: MethodOrPoly => - case _ => traverseApply(tree, Nil) - traverseChildren(tree) + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree, Nil) + traverseChildren(tree) + case tree: Block => + val saved = defsShadow + previousDefs = mutable.ListBuffer() :: previousDefs + try traverseChildren(tree) + finally + previousDefs = previousDefs.tail + defsShadow = saved + case tree: ValOrDefDef => + traverseChildren(tree) + if previousDefs.nonEmpty && !tree.symbol.isOneOf(TermParamOrAccessor) then + defsShadow ++= captures(tree.tpt).hidden.footprint + resultType(tree.symbol) = tree.tpt.nuType + previousDefs.head += tree case _ => traverseChildren(tree) end SepChecker diff --git a/project/Build.scala b/project/Build.scala index 34901a406d2e..5d68f478250e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1246,7 +1246,7 @@ object Build { settings(scala2LibraryBootstrappedSettings). settings( moduleName := "scala2-library-cc", - scalacOptions += "-Ycheck:all", + scalacOptions ++= Seq("-Ycheck:all", "-source", "3.7") ) lazy val scala2LibraryBootstrappedSettings = Seq( diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index cae2f4299e87..3cb57784ad95 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -683,7 +683,8 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz remaining -= 1 scout = scout.tail } - dropRightState(scout) + unsafeAssumeSeparate: + dropRightState(scout) } } diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index acf8faa7a969..d9b10129e3f9 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -1,50 +1,68 @@ --- Error: tests/neg-custom-args/captures/capt1.scala:6:11 -------------------------------------------------------------- -6 | () => if x == null then y else y // error +-- Error: tests/neg-custom-args/captures/capt1.scala:5:11 -------------------------------------------------------------- +5 | () => if x == null then y else y // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> C --- Error: tests/neg-custom-args/captures/capt1.scala:9:11 -------------------------------------------------------------- -9 | () => if x == null then y else y // error +-- Error: tests/neg-custom-args/captures/capt1.scala:8:11 -------------------------------------------------------------- +8 | () => if x == null then y else y // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type Matchable --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:16:2 ----------------------------------------- -16 | def f(y: Int) = if x == null then y else y // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:15:2 ----------------------------------------- +15 | def f(y: Int) = if x == null then y else y // error | ^ | Found: (y: Int) ->{x} Int | Required: Matchable -17 | f +16 | f | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:23:2 ----------------------------------------- -23 | class F(y: Int) extends A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:22:2 ----------------------------------------- +22 | class F(y: Int) extends A: // error | ^ | Found: A^{x} | Required: A -24 | def m() = if x == null then y else y -25 | F(22) +23 | def m() = if x == null then y else y +24 | F(22) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:28:2 ----------------------------------------- -28 | new A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:27:2 ----------------------------------------- +27 | new A: // error | ^ | Found: A^{x} | Required: A -29 | def m() = if x == null then y else y +28 | def m() = if x == null then y else y | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/capt1.scala:34:16 ------------------------------------------------------------- -34 | val z2 = h[() -> Cap](() => x) // error // error +-- Error: tests/neg-custom-args/captures/capt1.scala:36:16 ------------------------------------------------------------- +36 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^ - | Type variable X of method h cannot be instantiated to () -> (ex$15: caps.Exists) -> C^{ex$15} since - | the part C^{ex$15} of that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/capt1.scala:34:30 ------------------------------------------------------------- -34 | val z2 = h[() -> Cap](() => x) // error // error + | Type variable X of method h cannot be instantiated to () -> (ex$18: caps.Exists) -> C^{ex$18} since + | the part C^{ex$18} of that type captures the root capability `cap`. +-- Error: tests/neg-custom-args/captures/capt1.scala:36:30 ------------------------------------------------------------- +36 | val z2 = h[() -> Cap](() => x) // error // error | ^ | reference (x : C^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> (ex$15: caps.Exists) -> C^{ex$15} --- Error: tests/neg-custom-args/captures/capt1.scala:36:13 ------------------------------------------------------------- -36 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error + | of an enclosing function literal with expected type () -> (ex$18: caps.Exists) -> C^{ex$18} +-- Error: tests/neg-custom-args/captures/capt1.scala:38:13 ------------------------------------------------------------- +38 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ - | Type variable X of method h cannot be instantiated to box () ->{x} (ex$20: caps.Exists) -> C^{ex$20} since - | the part C^{ex$20} of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to box () ->{x} (ex$23: caps.Exists) -> C^{ex$23} since + | the part C^{ex$23} of that type captures the root capability `cap`. +-- Error: tests/neg-custom-args/captures/capt1.scala:43:7 -------------------------------------------------------------- +43 | if x == null then // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | This type hides capabilities {x} +-- Error: tests/neg-custom-args/captures/capt1.scala:44:12 ------------------------------------------------------------- +44 | () => x // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | This type hides capabilities {x} +-- Error: tests/neg-custom-args/captures/capt1.scala:47:2 -------------------------------------------------------------- +47 | x // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | This type hides capabilities {x} diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala index 8da7e633ca51..687073c3cdae 100644 --- a/tests/neg-custom-args/captures/capt1.scala +++ b/tests/neg-custom-args/captures/capt1.scala @@ -1,5 +1,4 @@ - - +import language.future // sepchecks on import annotation.retains class C def f(x: C @retains(caps.cap), y: C): () -> C = @@ -28,10 +27,21 @@ def h4(x: Cap, y: Int): A = new A: // error def m() = if x == null then y else y +def f1(c: Cap): () ->{c} c.type = () => c // ok + def foo() = val x: C @retains(caps.cap) = ??? def h[X](a: X)(b: X) = a + val z2 = h[() -> Cap](() => x) // error // error (() => C()) val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error + val z1: () => Cap = f1(x) + + val z4 = + if x == null then // error: separation + () => x // error: separation + else + () => C() + x // error: separation diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.scala b/tests/neg-custom-args/captures/cc-ex-conformance.scala index 16e13376c5b3..4920f26ac380 100644 --- a/tests/neg-custom-args/captures/cc-ex-conformance.scala +++ b/tests/neg-custom-args/captures/cc-ex-conformance.scala @@ -1,5 +1,6 @@ import language.experimental.captureChecking import caps.{Exists, Capability} +import language.future // sepchecks on class C @@ -15,7 +16,7 @@ def Test = val ex1: EX1 = ??? val ex2: EX2 = ??? val _: EX1 = ex1 - val _: EX2 = ex1 // ok + val _: EX2 = ex1 // error separation val _: EX1 = ex2 // ok val ex3: EX3 = ??? diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 67685d5663b8..e45a8dad6092 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -1,29 +1,29 @@ --- Error: tests/neg-custom-args/captures/i15772.scala:19:26 ------------------------------------------------------------ -19 | val c : C^{x} = new C(x) // error +-- Error: tests/neg-custom-args/captures/i15772.scala:21:26 ------------------------------------------------------------ +21 | val c : C^{x} = new C(x) // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Int --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:20:46 --------------------------------------- -20 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:22:46 --------------------------------------- +22 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error | ^^^^^^^ | Found: (C{val arg: C^}^{c} => Unit) ->{c} Unit | Required: (C^ => Unit) -> Unit | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/i15772.scala:26:26 ------------------------------------------------------------ -26 | val c : C^{x} = new C(x) // error +-- Error: tests/neg-custom-args/captures/i15772.scala:28:26 ------------------------------------------------------------ +28 | val c : C^{x} = new C(x) // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Int --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:27:35 --------------------------------------- -27 | val boxed2 : Observe[C^] = box2(c) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:29:35 --------------------------------------- +29 | val boxed2 : Observe[C^] = box2(c) // error | ^^^^^^^ | Found: (C{val arg: C^}^{c} => Unit) ->{c} Unit | Required: (C^ => Unit) -> Unit | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:34 --------------------------------------- -33 | val boxed2 : Observe[C]^ = box2(c) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:35:34 --------------------------------------- +35 | val boxed2 : Observe[C]^ = box2(c) // error | ^ | Found: box C^ | Required: box C{val arg: C^?}^? @@ -32,8 +32,8 @@ | cannot be included in capture set ? | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- -44 | x: (() -> Unit) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:46:2 ---------------------------------------- +46 | x: (() -> Unit) // error | ^ | Found: (x : () ->{filesList, sayHello} Unit) | Required: () -> Unit diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index a054eac835c1..c6e1d8693815 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -1,3 +1,5 @@ +import language.future // sepchecks on + type Observe[T] = (T => Unit) -> Unit def unsafe(cap: C^) = cap.bad() diff --git a/tests/neg-custom-args/captures/sep-compose.check b/tests/neg-custom-args/captures/sep-compose.check index 7ecab087904e..d763a180b9ed 100644 --- a/tests/neg-custom-args/captures/sep-compose.check +++ b/tests/neg-custom-args/captures/sep-compose.check @@ -92,10 +92,10 @@ 40 | p1(f) // error | ^ | Separation failure: argument of type (f : () ->{a} Unit) - | to method apply: (v1: T1): R + | to a function of type (x$0: () => Unit) ->{f} Unit | corresponds to capture-polymorphic formal parameter x$0 of type () => Unit | and captures {f, a, io}, but these capabilities are also passed separately - | in the function prefix with type (p1 : (x$0: () => Unit) ->{f} Unit). + | in the function prefix. | | Capture set of function prefix : {p1} | Hidden set of current argument : {f} diff --git a/tests/neg-custom-args/captures/sep-use.check b/tests/neg-custom-args/captures/sep-use.check new file mode 100644 index 000000000000..9379c29fc950 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use.check @@ -0,0 +1,24 @@ +-- Error: tests/neg-custom-args/captures/sep-use.scala:7:10 ------------------------------------------------------------ +7 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of value x with type () => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:13:10 ----------------------------------------------------------- +13 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method x with result type () => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:19:10 ----------------------------------------------------------- +19 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method xx with result type (y: Int) => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:25:10 ----------------------------------------------------------- +25 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method xxx with result type Object^. + | This type hides capabilities {io} diff --git a/tests/neg-custom-args/captures/sep-use.scala b/tests/neg-custom-args/captures/sep-use.scala new file mode 100644 index 000000000000..80be5073d06e --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use.scala @@ -0,0 +1,27 @@ +import caps.cap +import language.future // sepchecks on + +def test1(io: Object^): Unit = + + val x: () => Unit = () => println(io) + println(io) // error + println(x) // ok + +def test2(io: Object^): Unit = + + def x: () => Unit = () => println(io) + println(io) // error + println(x) // ok + +def test3(io: Object^): Unit = + + def xx: (y: Int) => Unit = _ => println(io) + println(io) // error + println(xx(2)) // ok + +def test4(io: Object^): Unit = + + def xxx(y: Int): Object^ = io + println(io) // error + println(xxx(2)) // ok + diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala new file mode 100644 index 000000000000..dc485196ac79 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -0,0 +1,28 @@ +import language.future // sepchecks on + +def test1(c: Object^, f: Object^ => Object^) = + def cc: Object^ = c + val x1 = + { f(cc) } // ok + val x2 = + f(cc) // ok + val x3: Object^ = + f(cc) // ok + val x4: Object^ = + { f(c) } // error + +def test2(c: Object^, f: Object^ ->{c} Object^) = + def cc: Object^ = c + val x1 = + { f(cc) } // error // error + val x4: Object^ = + { f(c) } // error // error + + + + + + + + + diff --git a/tests/pos-custom-args/captures/capt1.scala b/tests/pos-custom-args/captures/capt1.scala index e3f5c20e724e..34e9e40e7fdb 100644 --- a/tests/pos-custom-args/captures/capt1.scala +++ b/tests/pos-custom-args/captures/capt1.scala @@ -1,3 +1,6 @@ +import language.future // sepchecks on +import caps.unsafe.unsafeAssumeSeparate + class C type Cap = C^ def f1(c: Cap): () ->{c} c.type = () => c // ok @@ -22,6 +25,9 @@ def foo(): C^ = val z1: () => Cap = f1(x) def h[X](a: X)(b: X) = a - val z2 = - if x == null then () => x else () => C() - x \ No newline at end of file + val z2 = unsafeAssumeSeparate: + if x == null then + () => x + else + () => C() + unsafeAssumeSeparate(x) \ No newline at end of file diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala index dd6417042339..387616e023ec 100644 --- a/tests/pos-custom-args/captures/skolems2.scala +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -1,3 +1,5 @@ +import language.future // sepchecks on + def Test(c: Object^, f: Object^ => Object^) = def cc: Object^ = c val x1 = From 70074c4eb518cabb9319a6474d8bc24265d45c25 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 19 Jan 2025 18:56:29 +0100 Subject: [PATCH 07/93] Address review comments --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 2 +- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 4 ++-- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 17 +++++------------ compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- .../annotation/internal/freshCapability.scala | 5 ++++- library/src/scala/caps.scala | 6 +++--- 7 files changed, 17 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 49eb73dd762e..7b12f98317cb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -53,7 +53,7 @@ object ccConfig: Feature.sourceVersion.stable != SourceVersion.`3.5` /** If true, turn on separation checking */ - def useFresh(using Context): Boolean = + def useSepChecks(using Context): Boolean = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) end ccConfig diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index d01fa4d11e4a..dd2f8c8b7b6b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -17,7 +17,7 @@ import CaptureSet.VarState /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, * as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities. - * If there are several annotations they come with an orderL + * If there are several annotations they come with an order: * `*` first, `.rd` next, `?` last. */ trait CaptureRef extends TypeProxy, ValueType: diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c5de4e97807e..54c07fe3874b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1487,7 +1487,7 @@ class CheckCaptures extends Recheck, SymTransformer: * 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. + * by the type variable instantiation. */ private def improveReadOnly(actual: Type, expected: Type)(using Context): Type = actual match case actual @ CapturingType(parent, refs) @@ -1816,7 +1816,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 ccConfig.useSepChecks 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/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 14c4c03e4115..889f05ce8308 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -42,7 +42,7 @@ object Fresh: object Cap: def apply(initialHidden: Refs = emptySet)(using Context): CaptureRef = - if ccConfig.useFresh then + if ccConfig.useSepChecks then AnnotatedType(defn.captureRoot.termRef, Annot(CaptureSet.HiddenSet(initialHidden))) else defn.captureRoot.termRef @@ -61,20 +61,13 @@ object Fresh: class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: thisMap => - var reach = false - - private def initHidden = - val ref = owner.termRef - if reach then - if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet - else - if ref.isTracked then SimpleIdentitySet(ref) else emptySet + private var reach = false override def apply(t: Type) = if variance <= 0 then t else t match case t: CaptureRef if t.isCap => - Cap(initHidden) + Cap(ownerToHidden(owner, reach)) case t @ CapturingType(_, refs) => val savedReach = reach if t.isBoxed then reach = true @@ -103,11 +96,11 @@ object Fresh: /** Maps cap to fresh */ def fromCap(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = - if ccConfig.useFresh then FromCap(owner)(tp) else tp + if ccConfig.useSepChecks then FromCap(owner)(tp) else tp /** Maps fresh to cap */ def toCap(tp: Type)(using Context): Type = - if ccConfig.useFresh then FromCap(NoSymbol).inverse(tp) else tp + if ccConfig.useSepChecks 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 diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index e6ab50dfb632..c6aef7225d16 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -432,7 +432,7 @@ 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 of the tree * @pre !(boxed && sym.exists) */ private def transformTT(tree: TypeTree, sym: Symbol, boxed: Boolean)(using Context): Unit = diff --git a/library/src/scala/annotation/internal/freshCapability.scala b/library/src/scala/annotation/internal/freshCapability.scala index a25eee4f4c6d..210220ec0a89 100644 --- a/library/src/scala/annotation/internal/freshCapability.scala +++ b/library/src/scala/annotation/internal/freshCapability.scala @@ -1,7 +1,10 @@ package scala.annotation package internal -/** An annotation used internally for fresh capability wrappers of `cap` +/** An annotation used internally for fresh capability wrappers of `cap`. + * A fresh capability is encoded as `caps.cap @freshCapability(...)` where + * `freshCapability(...)` is a special kind of annotation of type `Fresh.Annot` + * that contains a hidden set. */ class freshCapability extends StaticAnnotation diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 9d0a8883cde9..d150a1b43e82 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -43,9 +43,9 @@ 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`. + /** Read-only capabilities x.rd which appear as terms in @retains annotations are encoded + * as `caps.readOnlyCapability(x)`. When converted to CaptureRef types in capture sets + * they are represented as `x.type @annotation.internal.readOnlyCapability`. */ extension (x: Any) def readOnlyCapability: Any = x From 190aaca456ad6e09e7bef8779020dd279af1b071 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 20 Jan 2025 16:52:28 +0100 Subject: [PATCH 08/93] Use deep capturesets for separation checking. When checking whether two items overlap we should always check their deep capture sets. Buried aliases should count as well. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 +-- .../src/dotty/tools/dotc/cc/SepCheck.scala | 32 +++++++++++-------- tests/neg-custom-args/captures/lazyref.check | 4 +-- .../neg-custom-args/captures/sepchecks2.check | 21 ++++++++++++ .../neg-custom-args/captures/sepchecks2.scala | 10 ++++++ 5 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 tests/neg-custom-args/captures/sepchecks2.check create mode 100644 tests/neg-custom-args/captures/sepchecks2.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 863afaa0aaf9..0ec00b008f4d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1051,7 +1051,7 @@ object CaptureSet: def getElems(v: Var): Option[Refs] = elemsMap.get(v) /** Record elements, return whether this was allowed. - * By default, recording is allowed in regular both not in frozen states. + * By default, recording is allowed in regular but not in frozen states. */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -1062,7 +1062,7 @@ object CaptureSet: def getDeps(v: Var): Option[Deps] = depsMap.get(v) /** Record dependent sets, return whether this was allowed. - * By default, recording is allowed in regular both not in frozen states. + * By default, recording is allowed in regular but not in frozen states. */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index ecdb2cc93a82..e802bf4cc5d9 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -66,21 +66,28 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: recur(refs) end hidden + + /** Deduct the footprint of `sym` and `sym*` from `refs` */ + private def deductSym(sym: Symbol)(using Context) = + val ref = sym.termRef + if ref.isTrackableRef then refs -- CaptureSet(ref, ref.reach).elems.footprint + else refs + + /** Deduct the footprint of all captures of `deps` from `refs` */ + private def deductCapturesOf(deps: List[Tree])(using Context): Refs = + deps.foldLeft(refs): (refs, dep) => + refs -- captures(dep).footprint end extension /** The captures of an argument or prefix widened to the formal parameter, if * the latter contains a cap. */ private def formalCaptures(arg: Tree)(using Context): Refs = - val argType = arg.formalType.orElse(arg.nuType) - (if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) - .elems + arg.formalType.orElse(arg.nuType).deepCaptureSet.elems /** The captures of a node */ private def captures(tree: Tree)(using Context): Refs = - val tpe = tree.nuType - (if tree.formalType.hasUseAnnot then tpe.deepCaptureSet else tpe.captureSet) - .elems + tree.nuType.deepCaptureSet.elems private def sepApplyError(fn: Tree, args: List[Tree], argIdx: Int, overlap: Refs, hiddenInArg: Refs, footprints: List[(Refs, Int)], @@ -144,7 +151,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def sepUseError(tree: Tree, used: Refs, globalOverlap: Refs)(using Context): Unit = val individualChecks = for mdefs <- previousDefs.iterator; mdef <- mdefs.iterator yield - val hiddenByDef = captures(mdef.tpt).hidden + val hiddenByDef = captures(mdef.tpt).hidden.footprint val overlap = defUseOverlap(hiddenByDef, used, tree.symbol) if !overlap.isEmpty then def resultStr = if mdef.isInstanceOf[DefDef] then " result" else "" @@ -172,20 +179,16 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val footprints = mutable.ListBuffer[(Refs, Int)]((footprint, 0)) val indexedArgs = args.zipWithIndex - def subtractDeps(elems: Refs, arg: Tree): Refs = - deps(arg).foldLeft(elems): (elems, dep) => - elems -- captures(dep).footprint - for (arg, idx) <- indexedArgs do if !arg.needsSepCheck then - footprint = footprint ++ subtractDeps(captures(arg).footprint, arg) + footprint = footprint ++ captures(arg).footprint.deductCapturesOf(deps(arg)) footprints += ((footprint, idx + 1)) for (arg, idx) <- indexedArgs do if arg.needsSepCheck then val ac = formalCaptures(arg) val hiddenInArg = ac.hidden.footprint //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") - val overlap = subtractDeps(hiddenInArg.overlapWith(footprint), arg) + val overlap = hiddenInArg.overlapWith(footprint).deductCapturesOf(deps(arg)) if !overlap.isEmpty then sepApplyError(fn, args, idx, overlap, hiddenInArg, footprints.toList, deps) footprint ++= captures(arg).footprint @@ -267,7 +270,8 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case tree: ValOrDefDef => traverseChildren(tree) if previousDefs.nonEmpty && !tree.symbol.isOneOf(TermParamOrAccessor) then - defsShadow ++= captures(tree.tpt).hidden.footprint + capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hidden.footprint}") + defsShadow ++= captures(tree.tpt).hidden.footprint.deductSym(tree.symbol) resultType(tree.symbol) = tree.tpt.nuType previousDefs.head += tree case _ => diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 85a76bf5a87c..be8f5e56fb87 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -35,9 +35,9 @@ |and captures {cap2}, but this capability is also passed separately |in the function prefix with type (LazyRef[Int]{val elem: () ->{ref2*} Int} | (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}))^{ref2}. | - | Capture set of function prefix : {ref1, ref2} + | Capture set of function prefix : {ref1, ref2, ref2*} | Hidden set of current argument : {cap2} - | Footprint of function prefix : {ref1, ref2, cap1, cap2} + | Footprint of function prefix : {ref1, ref2, ref2*, cap1, cap2} | Hidden footprint of current argument : {cap2} | Declared footprint of current argument: {} | Undeclared overlap of footprints : {cap2} diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check new file mode 100644 index 000000000000..d224123eae53 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -0,0 +1,21 @@ +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:7:10 --------------------------------------------------------- +7 | println(c) // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of value xs with type List[box () => Unit]. + | This type hides capabilities {xs*, c} +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:10:33 -------------------------------------------------------- +10 | foo((() => println(c)) :: Nil, c) // error + | ^ + | Separation failure: argument of type (c : Object^) + | to method foo: (xs: List[box () => Unit], y: Object^): Nothing + | corresponds to capture-polymorphic formal parameter y of type Object^ + | and captures {c}, but this capability is also passed separately + | in the first argument with type List[box () ->{c} Unit]. + | + | Capture set of first argument : {c} + | Hidden set of current argument : {c} + | Footprint of first argument : {c} + | Hidden footprint of current argument : {c} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {c} diff --git a/tests/neg-custom-args/captures/sepchecks2.scala b/tests/neg-custom-args/captures/sepchecks2.scala new file mode 100644 index 000000000000..614c7a2d9750 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks2.scala @@ -0,0 +1,10 @@ +import language.future // sepchecks on + +def foo(xs: List[() => Unit], y: Object^) = ??? + +def Test(c: Object^) = + val xs: List[() => Unit] = (() => println(c)) :: Nil + println(c) // error + +def Test2(c: Object^) = + foo((() => println(c)) :: Nil, c) // error From 67d42bdc2a2f291dfb731fb5afec97b8ed7b2b6c Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 21 Jan 2025 00:08:24 +0100 Subject: [PATCH 09/93] Cache derived reach, readOnly, and maybe capabilities This is necessary since capability sets are IdentitySets. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 24 ++++++++----- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 13 +++++++ tests/neg-custom-args/captures/i21614.check | 15 ++++---- tests/neg-custom-args/captures/reaches.check | 34 +++++++++++++++++-- tests/neg-custom-args/captures/reaches.scala | 4 +-- 5 files changed, 69 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 7b12f98317cb..0e26ea0aa9aa 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -463,7 +463,10 @@ extension (tp: Type) def reach(using Context): CaptureRef = tp match case tp @ AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.MaybeCapabilityAnnot => - tp.derivedAnnotatedType(tp1.reach, annot) + tp1.reach.maybe + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReadOnlyCapabilityAnnot => + tp1.reach.readOnly case tp @ AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.ReachCapabilityAnnot => tp @@ -476,9 +479,8 @@ extension (tp: Type) */ 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) + if annot.symbol == defn.MaybeCapabilityAnnot => + tp1.readOnly.maybe case tp @ AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.ReadOnlyCapabilityAnnot => tp @@ -710,17 +712,23 @@ object CapsOfApply: case TypeApply(capsOf, arg :: Nil) if capsOf.symbol == defn.Caps_capsOf => Some(arg) case _ => None -abstract class AnnotatedCapability(annot: Context ?=> ClassSymbol): +abstract class AnnotatedCapability(annotCls: Context ?=> ClassSymbol): def apply(tp: Type)(using Context): AnnotatedType = assert(tp.isTrackableRef) tp match - case AnnotatedType(_, annot) => assert(!unwrappable.contains(annot.symbol)) + case AnnotatedType(_, annot) => + assert(!unwrappable.contains(annot.symbol), i"illegal combination of derived capabilities: $annotCls over ${annot.symbol}") case _ => - AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) + tp match + case tp: CaptureRef => tp.derivedRef(annotCls) + case _ => AnnotatedType(tp, Annotation(annotCls, util.Spans.NoSpan)) + def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match - case AnnotatedType(parent: CaptureRef, ann) if ann.hasSymbol(annot) => Some(parent) + case AnnotatedType(parent: CaptureRef, ann) if ann.hasSymbol(annotCls) => Some(parent) case _ => None + protected def unwrappable(using Context): Set[Symbol] +end AnnotatedCapability /** An extractor for `ref @maybeCapability`, which is used to express * the maybe capability `ref?` as a type. diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index dd2f8c8b7b6b..d969aa7f90db 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -14,6 +14,7 @@ import Periods.NoRunId import compiletime.uninitialized import StdNames.nme import CaptureSet.VarState +import Annotations.Annotation /** 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. @@ -24,6 +25,18 @@ trait CaptureRef extends TypeProxy, ValueType: private var myCaptureSet: CaptureSet | Null = uninitialized private var myCaptureSetRunId: Int = NoRunId private var mySingletonCaptureSet: CaptureSet.Const | Null = null + private var myDerivedRefs: List[AnnotatedType] = Nil + + /** A derived reach, readOnly or maybe reference. Derived references are cached. */ + def derivedRef(annotCls: ClassSymbol)(using Context): AnnotatedType = + def recur(refs: List[AnnotatedType]): AnnotatedType = refs match + case ref :: refs1 => + if ref.annot.symbol == annotCls then ref else recur(refs1) + case Nil => + val derived = AnnotatedType(this, Annotation(annotCls, util.Spans.NoSpan)) + myDerivedRefs = derived :: myDerivedRefs + derived + recur(myDerivedRefs) /** Is the reference tracked? This is true if it can be tracked and the capture * set of the underlying type is not always empty. diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index 109283eae01f..aa60c2eaa366 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -1,11 +1,8 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:12 --------------------------------------- +-- [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) ->{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 + | ^ + | 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 --------------------------------------- @@ -14,7 +11,7 @@ |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 reference ex$16.rd - |cannot be included in outer capture set ? + |Note that the universal capability `cap.rd` + |cannot be included in capture set ? | | 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 ef755ebfcbd2..6eff8716838f 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -56,15 +56,45 @@ | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/reaches.scala:79:10 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error +79 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation -- Error: tests/neg-custom-args/captures/reaches.scala:79:13 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error +79 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation +-- Error: tests/neg-custom-args/captures/reaches.scala:79:31 ----------------------------------------------------------- +79 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck + | ^ + | Separation failure: argument of type (x$0: A) ->{y} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter g of type box A^? => box A^? + | and captures {ps*}, but this capability is also passed separately + | in the first argument with type (x$0: A) ->{x} box A^?. + | + | Capture set of first argument : {x} + | Hidden set of current argument : {y} + | Footprint of first argument : {x, ps*} + | Hidden footprint of current argument : {y, ps*} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {ps*} +-- Error: tests/neg-custom-args/captures/reaches.scala:82:31 ----------------------------------------------------------- +82 | ps.map((x, y) => compose1(x, y)) // error sepcheck + | ^ + | Separation failure: argument of type (x$0: A) ->{y} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter g of type box A^? => box A^? + | and captures {ps*}, but this capability is also passed separately + | in the first argument with type (x$0: A) ->{x} box A^?. + | + | Capture set of first argument : {x} + | Hidden set of current argument : {y} + | Footprint of first argument : {x, ps*} + | Hidden footprint of current argument : {y, ps*} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {ps*} -- Error: tests/neg-custom-args/captures/reaches.scala:61:31 ----------------------------------------------------------- 61 | val leaked = usingFile[File^{id*}]: f => // error | ^^^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index 34f05340a1e7..c82ada78f17b 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -76,7 +76,7 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = z => g(f(z)) def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) // error // error + ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck def mapCompose2[A](@use ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) + ps.map((x, y) => compose1(x, y)) // error sepcheck From b1da91a4af70c837b12ca6b932024c8ea73dccd7 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 21 Jan 2025 19:59:02 +0100 Subject: [PATCH 10/93] Avoid forming intersections of capture sets on refined type lookup --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 8 ++--- .../dotty/tools/dotc/cc/CheckCaptures.scala | 4 ++- .../dotty/tools/dotc/core/Definitions.scala | 5 +++ .../src/dotty/tools/dotc/core/Types.scala | 33 ++++++++++--------- .../tools/dotc/printing/PlainPrinter.scala | 4 +-- library/src/scala/caps.scala | 7 ++++ .../src/scala/collection/View.scala | 4 +-- .../captures/capt-depfun.check | 13 ++++++++ .../captures/capt-depfun.scala | 3 +- .../colltest5/CollectionStrawManCC5_1.scala | 8 +++++ 10 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 tests/neg-custom-args/captures/capt-depfun.check diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 0ec00b008f4d..52a7cd87f647 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1073,7 +1073,7 @@ object CaptureSet: * return whether this was allowed. By default, recording is allowed * but the special state VarState.Separate overrides this. */ - def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = + def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = elemsMap.get(hidden) match case None => elemsMap(hidden) = hidden.elems case _ => @@ -1112,7 +1112,7 @@ object CaptureSet: */ @sharable object Separate extends Closed: - override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = false + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = false /** A special state that turns off recording of elements. Used only * in `addSub` to prevent cycles in recordings. @@ -1122,14 +1122,14 @@ object CaptureSet: override def putElems(v: Var, refs: Refs) = true override def putDeps(v: Var, deps: Deps) = true override def rollBack(): Unit = () - override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = true + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true /** A closed state that turns off recording of hidden elements (but allows * adding them). Used in `mightAccountFor`. */ @sharable private[CaptureSet] object ClosedUnrecorded extends Closed: - override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = true + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true end VarState diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 54c07fe3874b..f6f0b8c07d9b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -24,6 +24,7 @@ import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} import Existential.derivedExistentialType +import Annotations.Annotation /** The capture checker */ object CheckCaptures: @@ -785,7 +786,8 @@ class CheckCaptures extends Recheck, SymTransformer: for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol if !getter.is(Private) && getter.hasTrackedParts then - refined = RefinedType(refined, getterName, argType.unboxed) // Yichen you might want to check this + refined = RefinedType(refined, getterName, + AnnotatedType(argType.unboxed, Annotation(defn.RefineOverrideAnnot, util.Spans.NoSpan))) // Yichen you might want to check this allCaptures ++= argType.captureSet (refined, allCaptures) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 0a08c974090d..9e1272f76e08 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1071,6 +1071,7 @@ class Definitions { @tu lazy val UncheckedCapturesAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedCaptures") @tu lazy val UntrackedCapturesAnnot: ClassSymbol = requiredClass("scala.caps.untrackedCaptures") @tu lazy val UseAnnot: ClassSymbol = requiredClass("scala.caps.use") + @tu lazy val RefineOverrideAnnot: ClassSymbol = requiredClass("scala.caps.refineOverride") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature") @tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter") @@ -1111,6 +1112,10 @@ class Definitions { @tu lazy val MetaAnnots: Set[Symbol] = NonBeanMetaAnnots + BeanGetterMetaAnnot + BeanSetterMetaAnnot + // Set of annotations that are not printed in types except under -Yprint-debug + @tu lazy val SilentAnnots: Set[Symbol] = + Set(InlineParamAnnot, ErasedParamAnnot, RefineOverrideAnnot) + // A list of annotations that are commonly used to indicate that a field/method argument or return // type is not null. These annotations are used by the nullification logic in JavaNullInterop to // improve the precision of type nullification. diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index c9defa97d6ff..d6367bd01bf3 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -860,21 +860,24 @@ object Types extends TypeUtils { pinfo recoverable_& rinfo pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, jointInfo) } - else - val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] - val joint = pdenot.meet( - new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), - pre, - safeIntersection = ctx.base.pendingMemberSearches.contains(name)) - joint match - case joint: SingleDenotation - if isRefinedMethod - && (rinfo <:< joint.info - || name == nme.apply && defn.isFunctionType(tp.parent)) => - // use `rinfo` to keep the right parameter names for named args. See i8516.scala. - joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) - case _ => - joint + else rinfo match + case AnnotatedType(rinfo1, ann) if ann.symbol == defn.RefineOverrideAnnot => + pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, rinfo1) + case _ => + val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] + val joint = pdenot.meet( + new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), + pre, + safeIntersection = ctx.base.pendingMemberSearches.contains(name)) + joint match + case joint: SingleDenotation + if isRefinedMethod + && (rinfo <:< joint.info + || name == nme.apply && defn.isFunctionType(tp.parent)) => + // use `rinfo` to keep the right parameter names for named args. See i8516.scala. + joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) + case _ => + joint } def goApplied(tp: AppliedType, tycon: HKTypeLambda) = diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 94656cc33bb2..3f086b28656c 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -310,8 +310,8 @@ class PlainPrinter(_ctx: Context) extends Printer { toTextGlobal(tp.resultType) } case AnnotatedType(tpe, annot) => - if annot.symbol == defn.InlineParamAnnot || annot.symbol == defn.ErasedParamAnnot - then toText(tpe) + if defn.SilentAnnots.contains(annot.symbol) && !printDebug then + toText(tpe) else if (annot.symbol == defn.IntoAnnot || annot.symbol == defn.IntoParamAnnot) && !printDebug then atPrec(GlobalPrec)( Str("into ") ~ toText(tpe) ) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index d150a1b43e82..034e9ad54b53 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -70,6 +70,13 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class use extends annotation.StaticAnnotation + /** An annotation placed on a refinement created by capture checking. + * Refinements with this annotation unconditionally override any + * info vfrom the parent type, so no intersection needs to be formed. + * This could be useful for tracked parameters as well. + */ + final class refineOverride extends annotation.StaticAnnotation + object unsafe: extension [T](x: T) diff --git a/scala2-library-cc/src/scala/collection/View.scala b/scala2-library-cc/src/scala/collection/View.scala index 132934dbe3bd..c5d582eb2a11 100644 --- a/scala2-library-cc/src/scala/collection/View.scala +++ b/scala2-library-cc/src/scala/collection/View.scala @@ -150,10 +150,8 @@ object View extends IterableFactory[View] { object Filter { def apply[A](underlying: Iterable[A]^, p: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, p} = underlying match { - case filter: Filter[A] if filter.isFlipped == isFlipped => + case filter: Filter[A]^{underlying} if filter.isFlipped == isFlipped => new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) - .asInstanceOf[Filter[A]^{underlying, p}] - // !!! asInstanceOf needed once paths were added, see path-patmat-should-be-pos.scala for minimization case _ => new Filter(underlying, p, isFlipped) } } diff --git a/tests/neg-custom-args/captures/capt-depfun.check b/tests/neg-custom-args/captures/capt-depfun.check new file mode 100644 index 000000000000..0ffa9588490a --- /dev/null +++ b/tests/neg-custom-args/captures/capt-depfun.check @@ -0,0 +1,13 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt-depfun.scala:10:43 ---------------------------------- +10 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error sepcheck + | ^^^^^^^ + | Found: Str^{} ->{ac, y, z} Str^{y, z} + | Required: Str^{y, z} ->{fresh} Str^{y, z} + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/capt-depfun.scala:10:24 ------------------------------------------------------- +10 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error sepcheck + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: Str^{y, z} => Str^{y, z} captures a root element hiding {ac, y, z} + | and also refers to {y, z}. + | The two sets overlap at {y, z} diff --git a/tests/neg-custom-args/captures/capt-depfun.scala b/tests/neg-custom-args/captures/capt-depfun.scala index 20226b239198..b259d7d19a08 100644 --- a/tests/neg-custom-args/captures/capt-depfun.scala +++ b/tests/neg-custom-args/captures/capt-depfun.scala @@ -1,4 +1,5 @@ import annotation.retains +import language.future // sepchecks on class C type Cap = C @retains(caps.cap) class Str @@ -6,4 +7,4 @@ class Str def f(y: Cap, z: Cap) = def g(): C @retains(y, z) = ??? val ac: ((x: Cap) => Str @retains(x) => Str @retains(x)) = ??? - val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error + val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error sepcheck diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index c22e1308db6d..43cc6d74b0e0 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -452,6 +452,14 @@ object CollectionStrawMan5 { this: Filter[A]^{underlying, p} => def iterator: Iterator[A]^{this} = underlying.iterator.filter(p) } + + object Filter: + def apply[A](underlying: Iterable[A]^, pp: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, pp} = + underlying match + case filter: Filter[A]^{underlying} => + new Filter(filter.underlying, a => filter.p(a) && pp(a)) + case _ => new Filter(underlying, pp) + case class Partition[A](val underlying: Iterable[A]^, p: A => Boolean) { self: Partition[A]^{underlying, p} => From 131f070b8124e1354de779155de83231074105cb Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 24 Jan 2025 13:25:41 +0100 Subject: [PATCH 11/93] Check separation of different parts of a declared type. --- .../src/dotty/tools/dotc/cc/SepCheck.scala | 154 ++++++++++++++++-- .../captures/capt-depfun.check | 8 +- .../captures/capt-depfun.scala | 2 +- tests/neg-custom-args/captures/reaches2.check | 35 ++-- tests/neg-custom-args/captures/reaches2.scala | 4 +- .../neg-custom-args/captures/sepchecks2.check | 37 ++++- .../neg-custom-args/captures/sepchecks2.scala | 20 ++- tests/pos-custom-args/captures/i15749a.scala | 3 +- .../colltest5/CollectionStrawManCC5_1.scala | 7 +- 9 files changed, 222 insertions(+), 48 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index e802bf4cc5d9..aa8216d216b2 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -10,11 +10,28 @@ import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* import CaptureSet.{Refs, emptySet, HiddenSet} import config.Printers.capt import StdNames.nme -import util.{SimpleIdentitySet, EqHashMap} +import util.{SimpleIdentitySet, EqHashMap, SrcPos} + +object SepChecker: + + /** Enumerates kinds of captures encountered so far */ + enum Captures: + case None + case Explicit // one or more explicitly declared captures + case Hidden // exacttly one hidden captures + case NeedsCheck // one hidden capture and one other capture (hidden or declared) + + def add(that: Captures): Captures = + if this == None then that + else if that == None then this + else if this == Explicit && that == Explicit then Explicit + else NeedsCheck + end Captures class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import tpd.* import checker.* + import SepChecker.* /** The set of capabilities that are hidden by a polymorphic result type * of some previous definition. @@ -52,21 +69,17 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: private def hidden(using Context): Refs = val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet - - def hiddenByElem(elem: CaptureRef): Refs = - if seen.add(elem) then elem match - case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) - case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) - case _ => emptySet - else emptySet - def recur(cs: Refs): Refs = (emptySet /: cs): (elems, elem) => - elems ++ hiddenByElem(elem) - + if seen.add(elem) then elems ++ hiddenByElem(elem, recur) + else elems recur(refs) end hidden + private def containsHidden(using Context): Boolean = + refs.exists: ref => + !hiddenByElem(ref, _ => emptySet).isEmpty + /** Deduct the footprint of `sym` and `sym*` from `refs` */ private def deductSym(sym: Symbol)(using Context) = val ref = sym.termRef @@ -79,6 +92,11 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: refs -- captures(dep).footprint end extension + private def hiddenByElem(ref: CaptureRef, recur: Refs => Refs)(using Context): Refs = ref match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) + case ReadOnlyCapability(ref1) => hiddenByElem(ref1, recur).map(_.readOnly) + case _ => emptySet + /** The captures of an argument or prefix widened to the formal parameter, if * the latter contains a cap. */ @@ -186,6 +204,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: for (arg, idx) <- indexedArgs do if arg.needsSepCheck then val ac = formalCaptures(arg) + checkType(arg.formalType, arg.srcPos, NoSymbol, " the argument's adapted type") val hiddenInArg = ac.hidden.footprint //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") val overlap = hiddenInArg.overlapWith(footprint).deductCapturesOf(deps(arg)) @@ -212,6 +231,105 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if !overlap.isEmpty then sepUseError(tree, usedFootprint, overlap) + def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = + checkType(tpt.nuType, tpt.srcPos, sym, "") + + /** Check that all parts of type `tpe` are separated. + * @param tpe the type to check + * @param pos position for error reporting + * @param sym if `tpe` is the (result-) type of a val or def, the symbol of + * this definition, otherwise NoSymbol. If `sym` exists we + * deduct its associated direct and reach capabilities everywhere + * from the capture sets we check. + * @param what a string describing what kind of type it is + */ + def checkType(tpe: Type, pos: SrcPos, sym: Symbol, what: String)(using Context): Unit = + + def checkParts(parts: List[Type]): Unit = + var footprint: Refs = emptySet + var hiddenSet: Refs = emptySet + var checked = 0 + for part <- parts do + + /** Report an error if `current` and `next` overlap. + * @param current the footprint or hidden set seen so far + * @param next the footprint or hidden set of the next part + * @param mapRefs a function over the capture set elements of the next part + * that returns the references of the same kind as `current` + * (i.e. the part's footprint or hidden set) + * @param prevRel a verbal description of current ("references or "hides") + * @param nextRel a verbal descriiption of next + */ + def checkSep(current: Refs, next: Refs, mapRefs: Refs => Refs, prevRel: String, nextRel: String): Unit = + val globalOverlap = current.overlapWith(next) + if !globalOverlap.isEmpty then + val (prevStr, prevRefs, overlap) = parts.iterator.take(checked) + .map: prev => + val prevRefs = mapRefs(prev.deepCaptureSet.elems).footprint.deductSym(sym) + (i", $prev , ", prevRefs, prevRefs.overlapWith(next)) + .dropWhile(_._3.isEmpty) + .nextOption + .getOrElse(("", current, globalOverlap)) + report.error( + em"""Separation failure in$what type $tpe. + |One part, $part , $nextRel ${CaptureSet(next)}. + |A previous part$prevStr $prevRel ${CaptureSet(prevRefs)}. + |The two sets overlap at ${CaptureSet(overlap)}.""", + pos) + + val partRefs = part.deepCaptureSet.elems + val partFootprint = partRefs.footprint.deductSym(sym) + val partHidden = partRefs.hidden.footprint.deductSym(sym) -- partFootprint + + checkSep(footprint, partHidden, identity, "references", "hides") + checkSep(hiddenSet, partHidden, _.hidden, "also hides", "hides") + checkSep(hiddenSet, partFootprint, _.hidden, "hides", "references") + + footprint ++= partFootprint + hiddenSet ++= partHidden + checked += 1 + end for + end checkParts + + object traverse extends TypeAccumulator[Captures]: + + /** A stack of part lists to check. We maintain this since immediately + * checking parts when traversing the type would check innermost to oputermost. + * But we want to check outermost parts first since this prioritized errors + * that are more obvious. + */ + var toCheck: List[List[Type]] = Nil + + private val seen = util.HashSet[Symbol]() + + def apply(c: Captures, t: Type) = + if variance < 0 then c + else + val t1 = t.dealias + t1 match + case t @ AppliedType(tycon, args) => + val c1 = foldOver(Captures.None, t) + if c1 == Captures.NeedsCheck then + toCheck = (tycon :: args) :: toCheck + c.add(c1) + case t @ CapturingType(parent, cs) => + val c1 = this(c, parent) + if cs.elems.containsHidden then c1.add(Captures.Hidden) + else if !cs.elems.isEmpty then c1.add(Captures.Explicit) + else c1 + case t: TypeRef if t.symbol.isAbstractOrParamType => + if seen.contains(t.symbol) then c + else + seen += t.symbol + apply(apply(c, t.prefix), t.info.bounds.hi) + case t => + foldOver(c, t) + + if !tpe.hasAnnotation(defn.UntrackedCapturesAnnot) then + traverse(Captures.None, tpe) + traverse.toCheck.foreach(checkParts) + end checkType + private def collectMethodTypes(tp: Type): List[TermLambda] = tp match case tp: MethodType => tp :: collectMethodTypes(tp.resType) case tp: PolyType => collectMethodTypes(tp.resType) @@ -231,7 +349,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: (formal, arg) <- mt.paramInfos.zip(args) dep <- formal.captureSet.elems.toList do - val referred = dep match + val referred = dep.stripReach match case dep: TermParamRef => argMap(dep.binder)(dep.paramNum) :: Nil case dep: ThisType if dep.cls == fn.symbol.owner => @@ -269,11 +387,13 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: defsShadow = saved case tree: ValOrDefDef => traverseChildren(tree) - if previousDefs.nonEmpty && !tree.symbol.isOneOf(TermParamOrAccessor) then - capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hidden.footprint}") - defsShadow ++= captures(tree.tpt).hidden.footprint.deductSym(tree.symbol) - resultType(tree.symbol) = tree.tpt.nuType - previousDefs.head += tree + if !tree.symbol.isOneOf(TermParamOrAccessor) then + checkType(tree.tpt, tree.symbol) + if previousDefs.nonEmpty then + capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hidden.footprint}") + defsShadow ++= captures(tree.tpt).hidden.footprint.deductSym(tree.symbol) + resultType(tree.symbol) = tree.tpt.nuType + previousDefs.head += tree case _ => traverseChildren(tree) end SepChecker diff --git a/tests/neg-custom-args/captures/capt-depfun.check b/tests/neg-custom-args/captures/capt-depfun.check index 0ffa9588490a..1e8a105afe5c 100644 --- a/tests/neg-custom-args/captures/capt-depfun.check +++ b/tests/neg-custom-args/captures/capt-depfun.check @@ -1,13 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt-depfun.scala:10:43 ---------------------------------- -10 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error sepcheck +10 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error | ^^^^^^^ | Found: Str^{} ->{ac, y, z} Str^{y, z} | Required: Str^{y, z} ->{fresh} Str^{y, z} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/capt-depfun.scala:10:24 ------------------------------------------------------- -10 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error sepcheck - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Separation failure: Str^{y, z} => Str^{y, z} captures a root element hiding {ac, y, z} - | and also refers to {y, z}. - | The two sets overlap at {y, z} diff --git a/tests/neg-custom-args/captures/capt-depfun.scala b/tests/neg-custom-args/captures/capt-depfun.scala index b259d7d19a08..acb707c8f6bf 100644 --- a/tests/neg-custom-args/captures/capt-depfun.scala +++ b/tests/neg-custom-args/captures/capt-depfun.scala @@ -7,4 +7,4 @@ class Str def f(y: Cap, z: Cap) = def g(): C @retains(y, z) = ??? val ac: ((x: Cap) => Str @retains(x) => Str @retains(x)) = ??? - val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error sepcheck + val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error diff --git a/tests/neg-custom-args/captures/reaches2.check b/tests/neg-custom-args/captures/reaches2.check index 1e921ee92072..74223963dde9 100644 --- a/tests/neg-custom-args/captures/reaches2.check +++ b/tests/neg-custom-args/captures/reaches2.check @@ -1,10 +1,25 @@ --- Error: tests/neg-custom-args/captures/reaches2.scala:8:10 ----------------------------------------------------------- -8 | ps.map((x, y) => compose1(x, y)) // error // error - | ^ - |reference ps* is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? --- Error: tests/neg-custom-args/captures/reaches2.scala:8:13 ----------------------------------------------------------- -8 | ps.map((x, y) => compose1(x, y)) // error // error - | ^ - |reference ps* is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? +-- Error: tests/neg-custom-args/captures/reaches2.scala:10:10 ---------------------------------------------------------- +10 | ps.map((x, y) => compose1(x, y)) // error // error // error + | ^ + |reference ps* is not included in the allowed capture set {} + |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? +-- Error: tests/neg-custom-args/captures/reaches2.scala:10:13 ---------------------------------------------------------- +10 | ps.map((x, y) => compose1(x, y)) // error // error // error + | ^ + |reference ps* is not included in the allowed capture set {} + |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? +-- Error: tests/neg-custom-args/captures/reaches2.scala:10:31 ---------------------------------------------------------- +10 | ps.map((x, y) => compose1(x, y)) // error // error // error + | ^ + | Separation failure: argument of type (x$0: A) ->{y} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter g of type box A^? => box A^? + | and captures {ps*}, but this capability is also passed separately + | in the first argument with type (x$0: A) ->{x} box A^?. + | + | Capture set of first argument : {x} + | Hidden set of current argument : {y} + | Footprint of first argument : {x, ps*} + | Hidden footprint of current argument : {y, ps*} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {ps*} diff --git a/tests/neg-custom-args/captures/reaches2.scala b/tests/neg-custom-args/captures/reaches2.scala index f2447b8c8795..9620d57d42e6 100644 --- a/tests/neg-custom-args/captures/reaches2.scala +++ b/tests/neg-custom-args/captures/reaches2.scala @@ -1,3 +1,5 @@ +import language.`3.8` // sepchecks on + class List[+A]: def map[B](f: A -> B): List[B] = ??? @@ -5,5 +7,5 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = z => g(f(z)) def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) // error // error + ps.map((x, y) => compose1(x, y)) // error // error // error diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check index d224123eae53..d27d5607ca70 100644 --- a/tests/neg-custom-args/captures/sepchecks2.check +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -1,11 +1,11 @@ --- Error: tests/neg-custom-args/captures/sepchecks2.scala:7:10 --------------------------------------------------------- -7 | println(c) // error - | ^ - | Separation failure: Illegal access to {c} which is hidden by the previous definition - | of value xs with type List[box () => Unit]. - | This type hides capabilities {xs*, c} --- Error: tests/neg-custom-args/captures/sepchecks2.scala:10:33 -------------------------------------------------------- -10 | foo((() => println(c)) :: Nil, c) // error +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:10:10 -------------------------------------------------------- +10 | println(c) // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of value xs with type List[box () => Unit]. + | This type hides capabilities {xs*, c} +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:13:33 -------------------------------------------------------- +13 | foo((() => println(c)) :: Nil, c) // error | ^ | Separation failure: argument of type (c : Object^) | to method foo: (xs: List[box () => Unit], y: Object^): Nothing @@ -19,3 +19,24 @@ | Hidden footprint of current argument : {c} | Declared footprint of current argument: {} | Undeclared overlap of footprints : {c} +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:14:10 -------------------------------------------------------- +14 | val x1: (Object^, Object^) = (c, c) // error + | ^^^^^^^^^^^^^^^^^^ + | Separation failure in type (box Object^, box Object^). + | One part, box Object^ , hides {c}. + | A previous part, box Object^ , also hides {c}. + | The two sets overlap at {c}. +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:15:10 -------------------------------------------------------- +15 | val x2: (Object^, Object^{d}) = (d, d) // error + | ^^^^^^^^^^^^^^^^^^^^^ + | Separation failure in type (box Object^, box Object^{d}). + | One part, box Object^{d} , references {d}. + | A previous part, box Object^ , hides {d}. + | The two sets overlap at {d}. +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:27:6 --------------------------------------------------------- +27 | bar((c, c)) // error + | ^^^^^^ + | Separation failure in the argument's adapted type type (box Object^, box Object^). + | One part, box Object^ , hides {c}. + | A previous part, box Object^ , also hides {c}. + | The two sets overlap at {c}. diff --git a/tests/neg-custom-args/captures/sepchecks2.scala b/tests/neg-custom-args/captures/sepchecks2.scala index 614c7a2d9750..f8176b989da8 100644 --- a/tests/neg-custom-args/captures/sepchecks2.scala +++ b/tests/neg-custom-args/captures/sepchecks2.scala @@ -1,10 +1,28 @@ import language.future // sepchecks on + def foo(xs: List[() => Unit], y: Object^) = ??? +def bar(x: (Object^, Object^)): Unit = ??? + def Test(c: Object^) = val xs: List[() => Unit] = (() => println(c)) :: Nil println(c) // error -def Test2(c: Object^) = +def Test2(c: Object^, d: Object^): Unit = foo((() => println(c)) :: Nil, c) // error + val x1: (Object^, Object^) = (c, c) // error + val x2: (Object^, Object^{d}) = (d, d) // error + +def Test3(c: Object^, d: Object^) = + val x: (Object^, Object^) = (c, d) // ok + +def Test4(c: Object^, d: Object^) = + val x: (Object^, Object^{c}) = (d, c) // ok + +def Test5(c: Object^, d: Object^): Unit = + bar((c, d)) // ok + +def Test6(c: Object^, d: Object^): Unit = + bar((c, c)) // error + diff --git a/tests/pos-custom-args/captures/i15749a.scala b/tests/pos-custom-args/captures/i15749a.scala index 184f980d6d70..d6f2d193dae2 100644 --- a/tests/pos-custom-args/captures/i15749a.scala +++ b/tests/pos-custom-args/captures/i15749a.scala @@ -1,5 +1,6 @@ import caps.cap import caps.use +import language.`3.7` // sepchecks on class Unit object u extends Unit @@ -13,7 +14,7 @@ def test = def wrapper[T](x: T): Wrapper[T] = Wrapper: [X] => (op: T ->{cap} X) => op(x) - def strictMap[A <: Top, B <: Top](mx: Wrapper[A])(f: A ->{cap} B): Wrapper[B] = + def strictMap[A <: Top, B <: Top](mx: Wrapper[A])(f: A ->{cap, mx*} B): Wrapper[B] = mx.value((x: A) => wrapper(f(x))) def force[A](thunk: Unit ->{cap} A): A = thunk(u) diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index 43cc6d74b0e0..4281f7a21128 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -6,6 +6,7 @@ import scala.reflect.ClassTag import annotation.unchecked.{uncheckedVariance, uncheckedCaptures} import annotation.tailrec import caps.cap +import caps.untrackedCaptures import language.`3.7` // sepchecks on /** A strawman architecture for new collections. It contains some @@ -68,11 +69,13 @@ object CollectionStrawMan5 { /** Base trait for strict collections */ trait Buildable[+A] extends Iterable[A] { protected def newBuilder: Builder[A, Repr] @uncheckedVariance - override def partition(p: A => Boolean): (Repr, Repr) = { + override def partition(p: A => Boolean): (Repr, Repr) @untrackedCaptures = + // Without untrackedCaptures this fails SepChecks.checkType. + // But this is probably an error in the hiding logic. + // TODO remove @untrackedCaptures and investigate val l, r = newBuilder iterator.foreach(x => (if (p(x)) l else r) += x) (l.result, r.result) - } // one might also override other transforms here to avoid generating // iterators if it helps efficiency. } From 3adf4b06de5338d4371d3e0fd03b8a0487548999 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 25 Jan 2025 19:27:54 +0100 Subject: [PATCH 12/93] Check that hidden parameters are annotated @consume TODO: - check that only @consume parameters flow to @consume parameters --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 4 +- .../src/dotty/tools/dotc/cc/SepCheck.scala | 89 +++++++++++++++---- .../dotty/tools/dotc/core/Definitions.scala | 2 + .../tools/dotc/util/SimpleIdentitySet.scala | 5 ++ library/src/scala/caps.scala | 4 + .../captures/box-adapt-contra.scala | 5 +- .../captures/capt-depfun.check | 9 +- .../captures/capt-depfun.scala | 3 +- .../captures/consume-overrides.scala | 15 ++++ .../captures/delayedRunops.check | 12 +-- .../captures/delayedRunops.scala | 5 +- .../captures/depfun-reach.check | 5 ++ .../captures/depfun-reach.scala | 2 +- .../captures/effect-swaps-explicit.scala | 4 +- .../captures/effect-swaps.check | 22 ++--- .../captures/effect-swaps.scala | 4 +- tests/neg-custom-args/captures/i15772.check | 5 ++ tests/neg-custom-args/captures/i15772.scala | 2 +- .../captures/i19330-alt2.scala | 2 +- tests/neg-custom-args/captures/i19330.check | 5 ++ tests/neg-custom-args/captures/i19330.scala | 2 +- tests/neg-custom-args/captures/i21442.check | 19 ++-- tests/neg-custom-args/captures/i21442.scala | 3 +- tests/neg-custom-args/captures/i22005.scala | 3 +- tests/neg-custom-args/captures/lazyref.scala | 2 +- tests/neg-custom-args/captures/reaches.check | 60 ++++++------- tests/neg-custom-args/captures/reaches.scala | 3 +- tests/neg-custom-args/captures/sep-use.scala | 10 +-- tests/neg-custom-args/captures/sep-use2.scala | 7 +- .../neg-custom-args/captures/sepchecks2.check | 14 ++- .../neg-custom-args/captures/sepchecks2.scala | 15 +++- .../neg-custom-args/captures/sepchecks3.scala | 12 +++ .../neg-custom-args/captures/sepchecks4.check | 25 ++++++ .../neg-custom-args/captures/sepchecks4.scala | 16 ++++ .../captures/unsound-reach-3.scala | 9 +- .../captures/unsound-reach-4.check | 19 ++-- .../captures/unsound-reach-4.scala | 7 +- .../captures/unsound-reach-6.scala | 5 +- tests/pos-custom-args/captures/cc-this.scala | 5 +- tests/pos-custom-args/captures/lazyref.scala | 5 +- tests/pos-custom-args/captures/reaches.scala | 7 +- tests/pos-custom-args/captures/skolems2.scala | 3 +- 42 files changed, 332 insertions(+), 123 deletions(-) create mode 100644 tests/neg-custom-args/captures/consume-overrides.scala create mode 100644 tests/neg-custom-args/captures/sepchecks3.scala create mode 100644 tests/neg-custom-args/captures/sepchecks4.check create mode 100644 tests/neg-custom-args/captures/sepchecks4.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f6f0b8c07d9b..2acc4da02ca5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1559,7 +1559,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def checkInheritedTraitParameters: Boolean = false - /** Check that overrides don't change the @use status of their parameters */ + /** Check that overrides don't change the @use or @consume status of their parameters */ override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit = def fail(msg: String) = report.error( @@ -1571,6 +1571,8 @@ class CheckCaptures extends Recheck, SymTransformer: do if param1.hasAnnotation(defn.UseAnnot) != param2.hasAnnotation(defn.UseAnnot) then fail(i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition") + if param1.hasAnnotation(defn.ConsumeAnnot) != param2.hasAnnotation(defn.ConsumeAnnot) then + fail(i"has a parameter ${param1.name} with different @consume 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/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index aa8216d216b2..d3e86a264c5d 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -28,6 +28,16 @@ object SepChecker: else NeedsCheck end Captures + /** The kind of checked type, used for composing error messages */ + enum TypeKind: + case Result(sym: Symbol, inferred: Boolean) + case Argument + + def dclSym = this match + case Result(sym, _) => sym + case _ => NoSymbol + end TypeKind + class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import tpd.* import checker.* @@ -204,7 +214,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: for (arg, idx) <- indexedArgs do if arg.needsSepCheck then val ac = formalCaptures(arg) - checkType(arg.formalType, arg.srcPos, NoSymbol, " the argument's adapted type") + checkType(arg.formalType, arg.srcPos, TypeKind.Argument) val hiddenInArg = ac.hidden.footprint //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") val overlap = hiddenInArg.overlapWith(footprint).deductCapturesOf(deps(arg)) @@ -232,18 +242,29 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: sepUseError(tree, usedFootprint, overlap) def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = - checkType(tpt.nuType, tpt.srcPos, sym, "") - - /** Check that all parts of type `tpe` are separated. - * @param tpe the type to check - * @param pos position for error reporting - * @param sym if `tpe` is the (result-) type of a val or def, the symbol of - * this definition, otherwise NoSymbol. If `sym` exists we - * deduct its associated direct and reach capabilities everywhere - * from the capture sets we check. - * @param what a string describing what kind of type it is - */ - def checkType(tpe: Type, pos: SrcPos, sym: Symbol, what: String)(using Context): Unit = + checkType(tpt.nuType, tpt.srcPos, + TypeKind.Result(sym, inferred = tpt.isInstanceOf[InferredTypeTree])) + + /** Check that all parts of type `tpe` are separated. */ + def checkType(tpe: Type, pos: SrcPos, kind: TypeKind)(using Context): Unit = + + def typeDescr = kind match + case TypeKind.Result(sym, inferred) => + def inferredStr = if inferred then " inferred" else "" + def resultStr = if sym.info.isInstanceOf[MethodicType] then " result" else "" + i" $sym's$inferredStr$resultStr" + case TypeKind.Argument => + " the argument's adapted type" + + def explicitRefs(tp: Type): Refs = tp match + case tp: (TermRef | ThisType) => SimpleIdentitySet(tp) + case AnnotatedType(parent, _) => explicitRefs(parent) + case AndType(tp1, tp2) => explicitRefs(tp1) ++ explicitRefs(tp2) + case OrType(tp1, tp2) => explicitRefs(tp1) ** explicitRefs(tp2) + case _ => emptySet + + def prune(refs: Refs): Refs = + refs.deductSym(kind.dclSym) -- explicitRefs(tpe) def checkParts(parts: List[Type]): Unit = var footprint: Refs = emptySet @@ -265,21 +286,21 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if !globalOverlap.isEmpty then val (prevStr, prevRefs, overlap) = parts.iterator.take(checked) .map: prev => - val prevRefs = mapRefs(prev.deepCaptureSet.elems).footprint.deductSym(sym) + val prevRefs = prune(mapRefs(prev.deepCaptureSet.elems).footprint) (i", $prev , ", prevRefs, prevRefs.overlapWith(next)) .dropWhile(_._3.isEmpty) .nextOption .getOrElse(("", current, globalOverlap)) report.error( - em"""Separation failure in$what type $tpe. + em"""Separation failure in$typeDescr type $tpe. |One part, $part , $nextRel ${CaptureSet(next)}. |A previous part$prevStr $prevRel ${CaptureSet(prevRefs)}. |The two sets overlap at ${CaptureSet(overlap)}.""", pos) val partRefs = part.deepCaptureSet.elems - val partFootprint = partRefs.footprint.deductSym(sym) - val partHidden = partRefs.hidden.footprint.deductSym(sym) -- partFootprint + val partFootprint = prune(partRefs.footprint) + val partHidden = prune(partRefs.hidden.footprint) -- partFootprint checkSep(footprint, partHidden, identity, "references", "hides") checkSep(hiddenSet, partHidden, _.hidden, "also hides", "hides") @@ -325,9 +346,43 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case t => foldOver(c, t) + def checkParameters() = + val badParams = mutable.ListBuffer[Symbol]() + def currentOwner = kind.dclSym.orElse(ctx.owner) + for hiddenRef <- prune(tpe.deepCaptureSet.elems.hidden.footprint) do + val refSym = hiddenRef.termSymbol + if refSym.is(TermParam) + && !refSym.hasAnnotation(defn.ConsumeAnnot) + && !refSym.info.derivesFrom(defn.Caps_SharedCapability) + && currentOwner.isContainedIn(refSym.owner) + then + badParams += refSym + if badParams.nonEmpty then + def paramsStr(params: List[Symbol]): String = (params: @unchecked) match + case p :: Nil => i"${p.name}" + case p :: p2 :: Nil => i"${p.name} and ${p2.name}" + case p :: ps => i"${p.name}, ${paramsStr(ps)}" + val (pluralS, singleS) = if badParams.tail.isEmpty then ("", "s") else ("s", "") + report.error( + em"""Separation failure:$typeDescr type $tpe hides parameter$pluralS ${paramsStr(badParams.toList)} + |The parameter$pluralS need$singleS to be annotated with @consume to allow this.""", + pos) + + def flagHiddenParams = + kind match + case TypeKind.Result(sym, _) => + !sym.isAnonymousFunction // we don't check return types of anonymous functions + && !sym.is(Case) // We don't check so far binders in patterns since they + // have inferred universal types. TODO come back to this; + // either infer more precise types for such binders or + // "see through them" when we look at hidden sets. + case TypeKind.Argument => + false + if !tpe.hasAnnotation(defn.UntrackedCapturesAnnot) then traverse(Captures.None, tpe) traverse.toCheck.foreach(checkParts) + if flagHiddenParams then checkParameters() end checkType private def collectMethodTypes(tp: Type): List[TermLambda] = tp match diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 9e1272f76e08..3fba3dbda082 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1010,6 +1010,7 @@ class Definitions { @tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_containsImpl: TermSymbol = CapsModule.requiredMethod("containsImpl") @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") + @tu lazy val Caps_SharedCapability: ClassSymbol = requiredClass("scala.caps.SharedCapability") /** The same as CaptureSet.universal but generated implicitly for references of Capability subtypes */ @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef.readOnly) @@ -1071,6 +1072,7 @@ class Definitions { @tu lazy val UncheckedCapturesAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedCaptures") @tu lazy val UntrackedCapturesAnnot: ClassSymbol = requiredClass("scala.caps.untrackedCaptures") @tu lazy val UseAnnot: ClassSymbol = requiredClass("scala.caps.use") + @tu lazy val ConsumeAnnot: ClassSymbol = requiredClass("scala.caps.consume") @tu lazy val RefineOverrideAnnot: ClassSymbol = requiredClass("scala.caps.refineOverride") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature") diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index b243145c9e5f..03392fe8cb23 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -42,6 +42,11 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { if (that.contains(x)) s else s + x } + def ** [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): SimpleIdentitySet[E] = + if this.size == 0 then this + else if that.size == 0 then that + else this.filter(that.contains) + def == [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): Boolean = this.size == that.size && forall(that.contains) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 034e9ad54b53..50497044fee8 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -18,6 +18,8 @@ import annotation.{experimental, compileTimeOnly, retainsCap} trait Mutable extends Capability + trait SharedCapability extends Capability + /** Carrier trait for capture set type parameters */ trait CapSet extends Any @@ -77,6 +79,8 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class refineOverride extends annotation.StaticAnnotation + final class consume extends annotation.StaticAnnotation + object unsafe: extension [T](x: T) diff --git a/tests/neg-custom-args/captures/box-adapt-contra.scala b/tests/neg-custom-args/captures/box-adapt-contra.scala index 2dc79a66d932..e74766228e45 100644 --- a/tests/neg-custom-args/captures/box-adapt-contra.scala +++ b/tests/neg-custom-args/captures/box-adapt-contra.scala @@ -1,4 +1,5 @@ -import language.experimental.captureChecking +import language.future // sepchecks on +import caps.consume trait Cap @@ -7,7 +8,7 @@ def useCap[X](x: X): (X -> Unit) -> Unit = ??? def test1(c: Cap^): Unit = val f: (Cap^{c} -> Unit) -> Unit = useCap[Cap^{c}](c) // error -def test2(c: Cap^, d: Cap^): Unit = +def test2(@consume c: Cap^, d: Cap^): Unit = def useCap1[X](x: X): (X => Unit) -> Unit = ??? val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // ok diff --git a/tests/neg-custom-args/captures/capt-depfun.check b/tests/neg-custom-args/captures/capt-depfun.check index 1e8a105afe5c..1faec0974a78 100644 --- a/tests/neg-custom-args/captures/capt-depfun.check +++ b/tests/neg-custom-args/captures/capt-depfun.check @@ -1,7 +1,12 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt-depfun.scala:10:43 ---------------------------------- -10 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt-depfun.scala:11:43 ---------------------------------- +11 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error: separatioon | ^^^^^^^ | Found: Str^{} ->{ac, y, z} Str^{y, z} | Required: Str^{y, z} ->{fresh} Str^{y, z} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/capt-depfun.scala:11:24 ------------------------------------------------------- +11 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error: separatioon + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: value dc's type Str^{y, z} => Str^{y, z} hides parameters y and z + | The parameters need to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/capt-depfun.scala b/tests/neg-custom-args/captures/capt-depfun.scala index acb707c8f6bf..f55f396cf681 100644 --- a/tests/neg-custom-args/captures/capt-depfun.scala +++ b/tests/neg-custom-args/captures/capt-depfun.scala @@ -1,5 +1,6 @@ import annotation.retains import language.future // sepchecks on + class C type Cap = C @retains(caps.cap) class Str @@ -7,4 +8,4 @@ class Str def f(y: Cap, z: Cap) = def g(): C @retains(y, z) = ??? val ac: ((x: Cap) => Str @retains(x) => Str @retains(x)) = ??? - val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error + val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error: separatioon diff --git a/tests/neg-custom-args/captures/consume-overrides.scala b/tests/neg-custom-args/captures/consume-overrides.scala new file mode 100644 index 000000000000..78f013349a31 --- /dev/null +++ b/tests/neg-custom-args/captures/consume-overrides.scala @@ -0,0 +1,15 @@ +import caps.consume + +trait A[X]: + def foo(@consume x: X): X + def bar(x: X): X + +trait B extends A[C]: + def foo(x: C): C // error + def bar(@consume x: C): C // error + +trait B2: + def foo(x: C): C + def bar(@consume x: C): C + +abstract class C extends A[C], B2 // error diff --git a/tests/neg-custom-args/captures/delayedRunops.check b/tests/neg-custom-args/captures/delayedRunops.check index 68da4672acf5..624dd1eaf079 100644 --- a/tests/neg-custom-args/captures/delayedRunops.check +++ b/tests/neg-custom-args/captures/delayedRunops.check @@ -1,14 +1,14 @@ --- Error: tests/neg-custom-args/captures/delayedRunops.scala:16:13 ----------------------------------------------------- -16 | runOps(ops1) // error +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:17:13 ----------------------------------------------------- +17 | runOps(ops1) // error | ^^^^ | reference ops* is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Unit --- Error: tests/neg-custom-args/captures/delayedRunops.scala:22:13 ----------------------------------------------------- -22 | runOps(ops1) // error +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:23:13 ----------------------------------------------------- +23 | runOps(ops1) // error | ^^^^ | Local reach capability ops1* leaks into capture scope of enclosing function --- Error: tests/neg-custom-args/captures/delayedRunops.scala:28:13 ----------------------------------------------------- -28 | runOps(ops1) // error +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:29:13 ----------------------------------------------------- +29 | runOps(ops1) // error | ^^^^ | reference ops* is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Unit diff --git a/tests/neg-custom-args/captures/delayedRunops.scala b/tests/neg-custom-args/captures/delayedRunops.scala index 191118fa19c9..1eaf6a0b3efe 100644 --- a/tests/neg-custom-args/captures/delayedRunops.scala +++ b/tests/neg-custom-args/captures/delayedRunops.scala @@ -1,5 +1,6 @@ import language.experimental.captureChecking -import caps.use +import language.future // sepchecks on +import caps.{use, consume} // ok def runOps(@use ops: List[() => Unit]): Unit = @@ -16,7 +17,7 @@ import caps.use runOps(ops1) // error // unsound: impure operation pretended pure - def delayedRunOps2(ops: List[() => Unit]): () ->{} Unit = + def delayedRunOps2(@consume ops: List[() => Unit]): () ->{} Unit = () => val ops1: List[() => Unit] = ops runOps(ops1) // error diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index 676ca7c5104f..2de01388def7 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -12,3 +12,8 @@ | Required: (xs: List[box () ->{io} Unit]) ->{fresh} List[() -> Unit] | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/depfun-reach.scala:12:17 ------------------------------------------------------ +12 | : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + |Separation failure: method foo's result type (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] hides parameter op + |The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/depfun-reach.scala b/tests/neg-custom-args/captures/depfun-reach.scala index 6c198ff8fd9f..e7affafe0240 100644 --- a/tests/neg-custom-args/captures/depfun-reach.scala +++ b/tests/neg-custom-args/captures/depfun-reach.scala @@ -9,7 +9,7 @@ def test(io: Object^, async: Object^) = compose(op) def foo[X](op: (xs: List[(X, () ->{io} Unit)]) => List[() ->{xs*} Unit]) - : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = + : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = // error op // error def boom(op: List[(() ->{async} Unit, () ->{io} Unit)]): List[() ->{} Unit] = diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.scala b/tests/neg-custom-args/captures/effect-swaps-explicit.scala index e440271ccf88..784c403d25fa 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.scala +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.scala @@ -1,4 +1,4 @@ - +import language.future // sepchecks on object boundary: @@ -14,7 +14,7 @@ end boundary import boundary.{Label, break} -trait Async extends caps.Capability +trait Async extends caps.SharedCapability object Async: def blocking[T](body: Async ?=> T): T = ??? diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index 48dc46c09821..28611959d905 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -1,29 +1,29 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:62:8 ---------------------------------- -61 | Result: -62 | Future: // error, type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:64:8 ---------------------------------- +63 | Result: +64 | Future: // error, type mismatch | ^ | Found: Result.Ok[box Future[box T^?]^{fr, contextual$1}] | Required: Result[Future[T], Nothing] -63 | fr.await.ok +65 | fr.await.ok |-------------------------------------------------------------------------------------------------------------------- |Inline stack trace |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |This location contains code that was inlined from effect-swaps.scala:39 -39 | boundary(Ok(body)) + |This location contains code that was inlined from effect-swaps.scala:41 +41 | boundary(Ok(body)) | ^^^^^^^^ -------------------------------------------------------------------------------------------------------------------- | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:72:10 --------------------------------- -72 | Future: fut ?=> // error: type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:74:10 --------------------------------- +74 | Future: fut ?=> // error: type mismatch | ^ | Found: Future[box T^?]^{fr, lbl} | Required: Future[box T^?]^? -73 | fr.await.ok +75 | fr.await.ok | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:15 ------------------------------------------------------ -66 | Result.make: // error: local reference leaks +-- Error: tests/neg-custom-args/captures/effect-swaps.scala:68:15 ------------------------------------------------------ +68 | Result.make: // error: local reference leaks | ^^^^^^^^^^^ |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]): | box Future[box T^?]^{fr, contextual$9} leaks into outer capture set of type parameter T of method make in object Result diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index 99c781b963c5..40ddd8917b2b 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -1,3 +1,5 @@ +import language.future // sepchecks on + object boundary: final class Label[-T] extends caps.Capability @@ -12,7 +14,7 @@ end boundary import boundary.{Label, break} -trait Async extends caps.Capability +trait Async extends caps.SharedCapability object Async: def blocking[T](body: Async ?=> T): T = ??? diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index e45a8dad6092..8b6534ce2c6b 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -39,3 +39,8 @@ | Required: () -> Unit | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/i15772.scala:34:10 ------------------------------------------------------------ +34 | def c : C^ = new C(x) // error separation + | ^^ + | Separation failure: method c's result type C^ hides parameter x + | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index c6e1d8693815..25dc2bbe1c2b 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -31,7 +31,7 @@ def main2(x: C^) : () -> Int = 0 def main3(x: C^) = - def c : C^ = new C(x) + def c : C^ = new C(x) // error separation val boxed2 : Observe[C]^ = box2(c) // error boxed2((cap: C^) => unsafe(c)) 0 diff --git a/tests/neg-custom-args/captures/i19330-alt2.scala b/tests/neg-custom-args/captures/i19330-alt2.scala index 3e52e3c65634..8c74e05185a2 100644 --- a/tests/neg-custom-args/captures/i19330-alt2.scala +++ b/tests/neg-custom-args/captures/i19330-alt2.scala @@ -10,6 +10,6 @@ trait Foo: def foo: this.T = val leaked = usingLogger[T]: l => // error - val t: () => Logger^ = () => l + val t: () => Logger^ = () => l // error separation t: T leaked diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index 78219e0316ee..5ca8811043a3 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -10,3 +10,8 @@ | Required: () ->{fresh} (ex$9: caps.Exists) -> Logger^{ex$9} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/i19330.scala:16:14 ------------------------------------------------------------ +16 | val t: () => Logger^ = () => l // error + | ^^^^^^^^^^^^^ + | Separation failure: value t's type () => (ex$5: caps.Exists) -> Logger^{ex$5} hides parameter l + | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index 23fcfa0ffc4f..bbc0b8483367 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -13,7 +13,7 @@ class Bar extends Foo: def foo(x: Foo): x.T = val leaked = usingLogger[x.T]: l => // error - val t: () => Logger^ = () => l + val t: () => Logger^ = () => l // error t: x.T leaked diff --git a/tests/neg-custom-args/captures/i21442.check b/tests/neg-custom-args/captures/i21442.check index 30becfea0215..72b2170dcba4 100644 --- a/tests/neg-custom-args/captures/i21442.check +++ b/tests/neg-custom-args/captures/i21442.check @@ -1,9 +1,14 @@ --- Error: tests/neg-custom-args/captures/i21442.scala:9:13 ------------------------------------------------------------- -9 | val io = x.unbox // error: local reach capability {x*} leaks - | ^^^^^^^ - | Local reach capability x* leaks into capture scope of method foo. - | To allow this, the parameter x should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/i21442.scala:17:14 ------------------------------------------------------------ -17 | val io = x1.unbox // error +-- Error: tests/neg-custom-args/captures/i21442.scala:10:13 ------------------------------------------------------------ +10 | val io = x.unbox // error: local reach capability {x*} leaks + | ^^^^^^^ + | Local reach capability x* leaks into capture scope of method foo. + | To allow this, the parameter x should be declared with a @use annotation +-- Error: tests/neg-custom-args/captures/i21442.scala:18:14 ------------------------------------------------------------ +18 | val io = x1.unbox // error | ^^^^^^^^ | Local reach capability x1* leaks into capture scope of method bar +-- Error: tests/neg-custom-args/captures/i21442.scala:17:10 ------------------------------------------------------------ +17 | val x1: Boxed[IO^] = x // error + | ^^^^^^^^^^ + | Separation failure: value x1's type Boxed[box IO^] hides parameter x + | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i21442.scala b/tests/neg-custom-args/captures/i21442.scala index c9fa7d152fae..8cfa0122cd1b 100644 --- a/tests/neg-custom-args/captures/i21442.scala +++ b/tests/neg-custom-args/captures/i21442.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +import language.future // sepchecks on trait IO: def use(): Unit case class Boxed[+T](unbox: T) @@ -13,6 +14,6 @@ def foo(x: Boxed[IO^]): Unit = // slightly different way. // But, no type error reported. def bar(x: Boxed[IO^]): Unit = - val x1: Boxed[IO^] = x + val x1: Boxed[IO^] = x // error val io = x1.unbox // error io.use() diff --git a/tests/neg-custom-args/captures/i22005.scala b/tests/neg-custom-args/captures/i22005.scala index a9dca999e42b..da8b9f6bc95c 100644 --- a/tests/neg-custom-args/captures/i22005.scala +++ b/tests/neg-custom-args/captures/i22005.scala @@ -1,3 +1,4 @@ +import language.future // sepchecks on import caps.* class IO @@ -5,4 +6,4 @@ class File(io: IO^) class Handler[C^]: def f(file: File^): File^{C^} = file // error - def g(file: File^{C^}): File^ = file // ok + def g(@consume file: File^{C^}): File^ = file // ok diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 52e274b65175..230b93edfea7 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -1,4 +1,4 @@ -import language.`3.7` // sepchecks on +import language.future // sepchecks on class CC type Cap = CC^ diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 6eff8716838f..008b22d9cd8e 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -1,12 +1,12 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:22:11 -------------------------------------- -22 | cur = (() => f.write()) :: Nil // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:23:11 -------------------------------------- +23 | cur = (() => f.write()) :: Nil // error | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: List[box () ->{xs*} Unit] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:33:7 --------------------------------------- -33 | (() => f.write()) :: Nil // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:34:7 --------------------------------------- +34 | (() => f.write()) :: Nil // error | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: box List[box () ->{xs*} Unit]^? @@ -15,58 +15,58 @@ | cannot be included in outer capture set {xs*} of value cur | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:36:6 ------------------------------------------------------------ -36 | var cur: List[Proc] = xs // error +-- Error: tests/neg-custom-args/captures/reaches.scala:37:6 ------------------------------------------------------------ +37 | var cur: List[Proc] = xs // error | ^ | Mutable variable cur cannot have type List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/reaches.scala:43:16 ----------------------------------------------------------- -43 | val cur = Ref[List[Proc]](xs) // error +-- Error: tests/neg-custom-args/captures/reaches.scala:44:16 ----------------------------------------------------------- +44 | val cur = Ref[List[Proc]](xs) // error | ^^^^^^^^^^ | Type variable T of constructor Ref cannot be instantiated to List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:45:35 -------------------------------------- -45 | val next: () => Unit = cur.get.head // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:46:35 -------------------------------------- +46 | val next: () => Unit = cur.get.head // error | ^^^^^^^^^^^^ | Found: () => Unit | Required: () ->{fresh} Unit | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:47:20 -------------------------------------- -47 | cur.set(cur.get.tail: List[Proc]) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:48:20 -------------------------------------- +48 | cur.set(cur.get.tail: List[Proc]) // error | ^^^^^^^^^^^^ | Found: List[box () => Unit] | Required: List[box () ->{fresh} Unit] | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:53:51 ----------------------------------------------------------- -53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error +-- Error: tests/neg-custom-args/captures/reaches.scala:54:51 ----------------------------------------------------------- +54 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error | ^ | Type variable A of constructor Id cannot be instantiated to box () => Unit since | that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/reaches.scala:55:6 ------------------------------------------------------------ -55 | id(() => f.write()) // error +-- Error: tests/neg-custom-args/captures/reaches.scala:56:6 ------------------------------------------------------------ +56 | id(() => f.write()) // error | ^^^^^^^^^^^^^^^^^^^ | Local reach capability id* leaks into capture scope of method test --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:62:27 -------------------------------------- -62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:63:27 -------------------------------------- +63 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error | ^^^^^ | Found: File^{f} | Required: File^{id*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:79:10 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck +-- Error: tests/neg-custom-args/captures/reaches.scala:80:10 ----------------------------------------------------------- +80 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/reaches.scala:79:13 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck +-- Error: tests/neg-custom-args/captures/reaches.scala:80:13 ----------------------------------------------------------- +80 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/reaches.scala:79:31 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck +-- Error: tests/neg-custom-args/captures/reaches.scala:80:31 ----------------------------------------------------------- +80 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Separation failure: argument of type (x$0: A) ->{y} box A^? | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C @@ -80,8 +80,8 @@ | Hidden footprint of current argument : {y, ps*} | Declared footprint of current argument: {} | Undeclared overlap of footprints : {ps*} --- Error: tests/neg-custom-args/captures/reaches.scala:82:31 ----------------------------------------------------------- -82 | ps.map((x, y) => compose1(x, y)) // error sepcheck +-- Error: tests/neg-custom-args/captures/reaches.scala:83:31 ----------------------------------------------------------- +83 | ps.map((x, y) => compose1(x, y)) // error sepcheck | ^ | Separation failure: argument of type (x$0: A) ->{y} box A^? | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C @@ -95,11 +95,11 @@ | Hidden footprint of current argument : {y, ps*} | Declared footprint of current argument: {} | Undeclared overlap of footprints : {ps*} --- Error: tests/neg-custom-args/captures/reaches.scala:61:31 ----------------------------------------------------------- -61 | val leaked = usingFile[File^{id*}]: f => // error +-- Error: tests/neg-custom-args/captures/reaches.scala:62:31 ----------------------------------------------------------- +62 | val leaked = usingFile[File^{id*}]: f => // error | ^^^ | id* cannot be tracked since its deep capture set is empty --- Error: tests/neg-custom-args/captures/reaches.scala:62:18 ----------------------------------------------------------- -62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error +-- Error: tests/neg-custom-args/captures/reaches.scala:63:18 ----------------------------------------------------------- +63 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error | ^^^ | id* cannot be tracked since its deep capture set is empty diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index c82ada78f17b..712069c7970d 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,4 +1,5 @@ import caps.use; import language.`3.7` // sepchecks on +import caps.consume class File: def write(): Unit = ??? @@ -32,7 +33,7 @@ def runAll1(@use xs: List[Proc]): Unit = cur.set: (() => f.write()) :: Nil // error -def runAll2(xs: List[Proc]): Unit = +def runAll2(@consume xs: List[Proc]): Unit = var cur: List[Proc] = xs // error while cur.nonEmpty do val next: () => Unit = cur.head diff --git a/tests/neg-custom-args/captures/sep-use.scala b/tests/neg-custom-args/captures/sep-use.scala index 80be5073d06e..53f3fddd18a7 100644 --- a/tests/neg-custom-args/captures/sep-use.scala +++ b/tests/neg-custom-args/captures/sep-use.scala @@ -1,25 +1,25 @@ -import caps.cap +import caps.{cap, consume} import language.future // sepchecks on -def test1(io: Object^): Unit = +def test1(@consume io: Object^): Unit = val x: () => Unit = () => println(io) println(io) // error println(x) // ok -def test2(io: Object^): Unit = +def test2(@consume io: Object^): Unit = def x: () => Unit = () => println(io) println(io) // error println(x) // ok -def test3(io: Object^): Unit = +def test3(@consume io: Object^): Unit = def xx: (y: Int) => Unit = _ => println(io) println(io) // error println(xx(2)) // ok -def test4(io: Object^): Unit = +def test4(@consume io: Object^): Unit = def xxx(y: Int): Object^ = io println(io) // error diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala index dc485196ac79..545b9d77efde 100644 --- a/tests/neg-custom-args/captures/sep-use2.scala +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -1,6 +1,7 @@ import language.future // sepchecks on +import caps.consume -def test1(c: Object^, f: Object^ => Object^) = +def test1(@consume c: Object^, f: Object^ => Object^) = def cc: Object^ = c val x1 = { f(cc) } // ok @@ -8,10 +9,10 @@ def test1(c: Object^, f: Object^ => Object^) = f(cc) // ok val x3: Object^ = f(cc) // ok - val x4: Object^ = + val x4: Object^ = // error { f(c) } // error -def test2(c: Object^, f: Object^ ->{c} Object^) = +def test2(@consume c: Object^, f: Object^ ->{c} Object^) = def cc: Object^ = c val x1 = { f(cc) } // error // error diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check index d27d5607ca70..89e91dad314b 100644 --- a/tests/neg-custom-args/captures/sepchecks2.check +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -22,14 +22,14 @@ -- Error: tests/neg-custom-args/captures/sepchecks2.scala:14:10 -------------------------------------------------------- 14 | val x1: (Object^, Object^) = (c, c) // error | ^^^^^^^^^^^^^^^^^^ - | Separation failure in type (box Object^, box Object^). + | Separation failure in value x1's type (box Object^, box Object^). | One part, box Object^ , hides {c}. | A previous part, box Object^ , also hides {c}. | The two sets overlap at {c}. -- Error: tests/neg-custom-args/captures/sepchecks2.scala:15:10 -------------------------------------------------------- 15 | val x2: (Object^, Object^{d}) = (d, d) // error | ^^^^^^^^^^^^^^^^^^^^^ - | Separation failure in type (box Object^, box Object^{d}). + | Separation failure in value x2's type (box Object^, box Object^{d}). | One part, box Object^{d} , references {d}. | A previous part, box Object^ , hides {d}. | The two sets overlap at {d}. @@ -40,3 +40,13 @@ | One part, box Object^ , hides {c}. | A previous part, box Object^ , also hides {c}. | The two sets overlap at {c}. +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:30:9 --------------------------------------------------------- +30 | val x: (Object^, Object^{c}) = (d, c) // error + | ^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: value x's type (box Object^, box Object^{c}) hides parameter d + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:33:9 --------------------------------------------------------- +33 | val x: (Object^, Object^) = (c, d) // error + | ^^^^^^^^^^^^^^^^^^ + | Separation failure: value x's type (box Object^, box Object^) hides parameters c and d + | The parameters need to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/sepchecks2.scala b/tests/neg-custom-args/captures/sepchecks2.scala index f8176b989da8..a9b88649820d 100644 --- a/tests/neg-custom-args/captures/sepchecks2.scala +++ b/tests/neg-custom-args/captures/sepchecks2.scala @@ -1,11 +1,11 @@ import language.future // sepchecks on - +import caps.consume def foo(xs: List[() => Unit], y: Object^) = ??? def bar(x: (Object^, Object^)): Unit = ??? -def Test(c: Object^) = +def Test(@consume c: Object^) = val xs: List[() => Unit] = (() => println(c)) :: Nil println(c) // error @@ -14,10 +14,10 @@ def Test2(c: Object^, d: Object^): Unit = val x1: (Object^, Object^) = (c, c) // error val x2: (Object^, Object^{d}) = (d, d) // error -def Test3(c: Object^, d: Object^) = +def Test3(@consume c: Object^, @consume d: Object^) = val x: (Object^, Object^) = (c, d) // ok -def Test4(c: Object^, d: Object^) = +def Test4(@consume c: Object^, @consume d: Object^) = val x: (Object^, Object^{c}) = (d, c) // ok def Test5(c: Object^, d: Object^): Unit = @@ -26,3 +26,10 @@ def Test5(c: Object^, d: Object^): Unit = def Test6(c: Object^, d: Object^): Unit = bar((c, c)) // error +def Test7(c: Object^, d: Object^) = + val x: (Object^, Object^{c}) = (d, c) // error + +def Test8(c: Object^, d: Object^) = + val x: (Object^, Object^) = (c, d) // error + + diff --git a/tests/neg-custom-args/captures/sepchecks3.scala b/tests/neg-custom-args/captures/sepchecks3.scala new file mode 100644 index 000000000000..cdc52920bb55 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks3.scala @@ -0,0 +1,12 @@ +import language.future // sepchecks on +import caps.consume + +def foo(xs: List[() => Unit], y: Object^) = ??? + +def bar(x: (Object^, Object^)): Unit = ??? + +def Test(c: Object^): Object^ = c // error + +def Test2(@consume c: Object^): Object^ = c // ok + +def Test3(c: Object^): List[Object^] = c :: Nil // error diff --git a/tests/neg-custom-args/captures/sepchecks4.check b/tests/neg-custom-args/captures/sepchecks4.check new file mode 100644 index 000000000000..ddf001719b95 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks4.check @@ -0,0 +1,25 @@ +-- Error: tests/neg-custom-args/captures/sepchecks4.scala:8:12 --------------------------------------------------------- +8 | val x: () => Unit = () => println(io) // error + | ^^^^^^^^^^ + | Separation failure: value x's type () => Unit hides parameter io + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks4.scala:7:25 --------------------------------------------------------- +7 |def bad(io: Object^): () => Unit = // error + | ^^^^^^^^^^ + | Separation failure: method bad's result type () => Unit hides parameter io + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks4.scala:12:25 -------------------------------------------------------- +12 | par(() => println(io))(() => println(io)) // error // (1) + | ^^^^^^^^^^^^^^^^^ + | Separation failure: argument of type () ->{io} Unit + | to method par: (op1: () => Unit)(op2: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter op2 of type () => Unit + | and captures {io}, but this capability is also passed separately + | in the first argument with type () ->{io} Unit. + | + | Capture set of first argument : {io} + | Hidden set of current argument : {io} + | Footprint of first argument : {io} + | Hidden footprint of current argument : {io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {io} diff --git a/tests/neg-custom-args/captures/sepchecks4.scala b/tests/neg-custom-args/captures/sepchecks4.scala new file mode 100644 index 000000000000..d44b31ca02dc --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks4.scala @@ -0,0 +1,16 @@ +import caps.cap +import language.future +import language.experimental.captureChecking + +def par(op1: () => Unit)(op2: () => Unit): Unit = () + +def bad(io: Object^): () => Unit = // error + val x: () => Unit = () => println(io) // error + x + +def test(io: Object^): Unit = + par(() => println(io))(() => println(io)) // error // (1) + + val f = bad(io) + par(f)(() => println(io)) // no error, but it is equivalent to (1) and should failimport caps.consume + diff --git a/tests/neg-custom-args/captures/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala index 0992dffb63ff..052f757da3a3 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -1,15 +1,16 @@ +import language.future // sepchecks on +import language.experimental.captureChecking +import caps.consume - -import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait File: def close(): Unit def withFile[R](path: String)(op: File^ => R): R = ??? trait Foo[+X]: - def use(x: File^): X + def use(@consume x: File^): X class Bar extends Foo[File^]: // error - def use(x: File^): File^ = x + def use(@consume x: File^): File^ = x def bad(): Unit = val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index 2d00eb0364e0..c4905cee258c 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -1,16 +1,23 @@ --- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:13:18 --------------------------------------------------- -13 |class Bar extends Foo[File^]: // error +-- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:16:18 --------------------------------------------------- +16 |class Bar extends Foo[File^]: // error | ^^^^^^^^^^ | Type variable X of trait Foo cannot be instantiated to File^ since | that type captures the root capability `cap`. --- [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). +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-4.scala:20:29 ------------------------------ +20 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). | ^^^^^^^ | Found: Bar^? | Required: Foo[box File^] | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:22:22 --------------------------------------------------- -22 | escaped = boom.use(f) // error +-- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:25:22 --------------------------------------------------- +25 | escaped = boom.use(f) // error | ^^^^^^^^^^^ | Local reach capability backdoor* leaks into capture scope of method bad +-- [E164] Declaration Error: tests/neg-custom-args/captures/unsound-reach-4.scala:17:6 --------------------------------- +17 | def use(@consume x: F): File^ = x // error @consume override + | ^ + |error overriding method use in trait Foo of type (x: File^): box File^; + | method use of type (x: File^): (ex$2: caps.Exists) -> File^{ex$2} has a parameter x with different @consume status than the corresponding parameter in the overridden definition + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index bba09c0286e3..108619a4780a 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -1,6 +1,9 @@ -import language.experimental.captureChecking; import language.`3.7` // sepchecks on +import language.future // sepchecks on +import language.experimental.captureChecking; +import caps.consume + trait File: def close(): Unit @@ -11,7 +14,7 @@ type F = File^ trait Foo[+X]: def use(x: F): X class Bar extends Foo[File^]: // error - def use(x: F): File^ = x + def use(@consume x: F): File^ = x // error @consume override def bad(): Unit = val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). diff --git a/tests/neg-custom-args/captures/unsound-reach-6.scala b/tests/neg-custom-args/captures/unsound-reach-6.scala index b7306dca4190..5e73407eb94a 100644 --- a/tests/neg-custom-args/captures/unsound-reach-6.scala +++ b/tests/neg-custom-args/captures/unsound-reach-6.scala @@ -1,6 +1,9 @@ +import language.future // sepchecks on +import caps.consume + class IO -def f(xs: List[() => Unit]): () => Unit = () => +def f(@consume xs: List[() => Unit]): () => Unit = () => println(xs.head) // error def test(io: IO^)(ys: List[() ->{io} Unit]) = diff --git a/tests/pos-custom-args/captures/cc-this.scala b/tests/pos-custom-args/captures/cc-this.scala index d9705df76c55..803470e270fa 100644 --- a/tests/pos-custom-args/captures/cc-this.scala +++ b/tests/pos-custom-args/captures/cc-this.scala @@ -1,8 +1,11 @@ +import caps.consume +import language.future // sepchecks on + class Cap extends caps.Capability def eff(using Cap): Unit = () -def test(using Cap) = +def test(using @consume cc: Cap) = class C(val x: () => Int): val y: C^ = this diff --git a/tests/pos-custom-args/captures/lazyref.scala b/tests/pos-custom-args/captures/lazyref.scala index 2e3a0030bcdc..ba31e0f6e908 100644 --- a/tests/pos-custom-args/captures/lazyref.scala +++ b/tests/pos-custom-args/captures/lazyref.scala @@ -1,3 +1,6 @@ +import language.future // sepchecks on +import caps.consume + class Cap extends caps.Capability class LazyRef[T](val elem: () => T): @@ -11,7 +14,7 @@ def map[A, B](ref: LazyRef[A]^, f: A => B): LazyRef[B]^{f, ref} = def mapc[A, B]: (ref: LazyRef[A]^, f: A => B) => LazyRef[B]^{f, ref} = (ref1, f1) => map[A, B](ref1, f1) -def test(cap1: Cap, cap2: Cap) = +def test(@consume cap1: Cap, @consume cap2: Cap) = def f(x: Int) = if cap1 == cap1 then x else 0 def g(x: Int) = if cap2 == cap2 then x else 0 val ref1 = LazyRef(() => f(0)) diff --git a/tests/pos-custom-args/captures/reaches.scala b/tests/pos-custom-args/captures/reaches.scala index cbe88e60020b..aad86ecbc3c4 100644 --- a/tests/pos-custom-args/captures/reaches.scala +++ b/tests/pos-custom-args/captures/reaches.scala @@ -1,4 +1,5 @@ -import caps.use +import language.future // sepchecks on +import caps.{use, consume} class C def f(xs: List[C^]) = @@ -36,7 +37,7 @@ def cons(x: Proc, xs: List[Proc]): List[() ->{x, xs*} Unit] = val y = x :: xs y -def addOneProc(xs: List[Proc]): List[Proc] = +def addOneProc(@consume xs: List[Proc]): List[Proc] = val x: Proc = () => println("hello") val result: List[() ->{x, xs*} Unit] = x :: xs result // OK, we can widen () ->{x, xs*} Unit to cap here. @@ -44,7 +45,7 @@ def addOneProc(xs: List[Proc]): List[Proc] = def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = z => g(f(z)) -def compose2[A, B, C](f: A => B, g: B => C): A => C = +def compose2[A, B, C](@consume f: A => B, @consume g: B => C): A => C = z => g(f(z)) //def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala index 387616e023ec..a891ad46616c 100644 --- a/tests/pos-custom-args/captures/skolems2.scala +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -1,6 +1,7 @@ import language.future // sepchecks on +import caps.consume -def Test(c: Object^, f: Object^ => Object^) = +def Test(@consume c: Object^, f: Object^ => Object^) = def cc: Object^ = c val x1 = { f(cc) } From ee2323905622101e415d60278c8c2b930912b36b Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Jan 2025 13:45:51 +0100 Subject: [PATCH 13/93] Check that only @consume parameters flow to @consume parameters --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 -- .../dotty/tools/dotc/cc/CheckCaptures.scala | 25 +++++----- .../src/dotty/tools/dotc/cc/SepCheck.scala | 46 ++++++++++--------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 22 +++++---- .../dotty/tools/dotc/core/Definitions.scala | 2 + .../neg-custom-args/captures/bad-uses-2.scala | 10 +++- .../captures/capt-depfun.check | 2 +- .../captures/depfun-reach.check | 2 +- tests/neg-custom-args/captures/i15772.check | 2 +- tests/neg-custom-args/captures/i19330.check | 2 +- tests/neg-custom-args/captures/i21442.check | 2 +- .../neg-custom-args/captures/sepchecks2.check | 6 +-- .../neg-custom-args/captures/sepchecks4.check | 4 +- .../neg-custom-args/captures/sepchecks5.check | 10 ++++ .../neg-custom-args/captures/sepchecks5.scala | 21 +++++++++ .../captures/unsound-reach-6.check | 15 ++++++ .../captures/unsound-reach-6.scala | 4 +- 17 files changed, 121 insertions(+), 58 deletions(-) create mode 100644 tests/neg-custom-args/captures/sepchecks5.check create mode 100644 tests/neg-custom-args/captures/sepchecks5.scala create mode 100644 tests/neg-custom-args/captures/unsound-reach-6.check diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 0e26ea0aa9aa..d64f3e81603a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -427,10 +427,6 @@ 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 2acc4da02ca5..df6eb2d385cc 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -655,11 +655,13 @@ class CheckCaptures extends Recheck, SymTransformer: * on method parameter symbols to the corresponding paramInfo types. */ override def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType = - val paramInfosWithUses = funtpe.paramInfos.zipWithConserve(funtpe.paramNames): (formal, pname) => - val param = meth.paramNamed(pname) - param.getAnnotation(defn.UseAnnot) match - case Some(ann) => AnnotatedType(formal, ann) - case _ => formal + val paramInfosWithUses = + funtpe.paramInfos.zipWithConserve(funtpe.paramNames): (formal, pname) => + val param = meth.paramNamed(pname) + def copyAnnot(tp: Type, cls: ClassSymbol) = param.getAnnotation(cls) match + case Some(ann) => AnnotatedType(tp, ann) + case _ => tp + copyAnnot(copyAnnot(formal, defn.UseAnnot), defn.ConsumeAnnot) funtpe.derivedLambdaType(paramInfos = paramInfosWithUses) /** Recheck applications, with special handling of unsafeAssumePure. @@ -687,7 +689,7 @@ class CheckCaptures extends Recheck, SymTransformer: val freshenedFormal = Fresh.fromCap(formal) val argType = recheck(arg, freshenedFormal) .showing(i"recheck arg $arg vs $freshenedFormal", capt) - if formal.hasUseAnnot then + if formal.hasAnnotation(defn.UseAnnot) 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) @@ -722,7 +724,7 @@ class CheckCaptures extends Recheck, SymTransformer: val qualCaptures = qualType.captureSet val argCaptures = for (argType, formal) <- argTypes.lazyZip(funType.paramInfos) yield - if formal.hasUseAnnot then argType.deepCaptureSet else argType.captureSet + if formal.hasAnnotation(defn.UseAnnot) then argType.deepCaptureSet else argType.captureSet appType match case appType @ CapturingType(appType1, refs) if qualType.exists @@ -1569,10 +1571,11 @@ class CheckCaptures extends Recheck, SymTransformer: (params1, params2) <- member.rawParamss.lazyZip(other.rawParamss) (param1, param2) <- params1.lazyZip(params2) do - if param1.hasAnnotation(defn.UseAnnot) != param2.hasAnnotation(defn.UseAnnot) then - fail(i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition") - if param1.hasAnnotation(defn.ConsumeAnnot) != param2.hasAnnotation(defn.ConsumeAnnot) then - fail(i"has a parameter ${param1.name} with different @consume status than the corresponding parameter in the overridden definition") + def checkAnnot(cls: ClassSymbol) = + if param1.hasAnnotation(cls) != param2.hasAnnotation(cls) then + fail(i"has a parameter ${param1.name} with different @${cls.name} status than the corresponding parameter in the overridden definition") + checkAnnot(defn.UseAnnot) + checkAnnot(defn.ConsumeAnnot) end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index d3e86a264c5d..7fe50eec02a6 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -11,6 +11,7 @@ import CaptureSet.{Refs, emptySet, HiddenSet} import config.Printers.capt import StdNames.nme import util.{SimpleIdentitySet, EqHashMap, SrcPos} +import tpd.* object SepChecker: @@ -31,7 +32,7 @@ object SepChecker: /** The kind of checked type, used for composing error messages */ enum TypeKind: case Result(sym: Symbol, inferred: Boolean) - case Argument + case Argument(arg: Tree) def dclSym = this match case Result(sym, _) => sym @@ -39,7 +40,6 @@ object SepChecker: end TypeKind class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: - import tpd.* import checker.* import SepChecker.* @@ -214,7 +214,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: for (arg, idx) <- indexedArgs do if arg.needsSepCheck then val ac = formalCaptures(arg) - checkType(arg.formalType, arg.srcPos, TypeKind.Argument) + checkType(arg.formalType, arg.srcPos, TypeKind.Argument(arg)) val hiddenInArg = ac.hidden.footprint //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") val overlap = hiddenInArg.overlapWith(footprint).deductCapturesOf(deps(arg)) @@ -252,9 +252,9 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case TypeKind.Result(sym, inferred) => def inferredStr = if inferred then " inferred" else "" def resultStr = if sym.info.isInstanceOf[MethodicType] then " result" else "" - i" $sym's$inferredStr$resultStr" - case TypeKind.Argument => - " the argument's adapted type" + i"$sym's$inferredStr$resultStr" + case TypeKind.Argument(_) => + "the argument's adapted" def explicitRefs(tp: Type): Refs = tp match case tp: (TermRef | ThisType) => SimpleIdentitySet(tp) @@ -292,7 +292,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: .nextOption .getOrElse(("", current, globalOverlap)) report.error( - em"""Separation failure in$typeDescr type $tpe. + em"""Separation failure in $typeDescr type $tpe. |One part, $part , $nextRel ${CaptureSet(next)}. |A previous part$prevStr $prevRel ${CaptureSet(prevRefs)}. |The two sets overlap at ${CaptureSet(overlap)}.""", @@ -346,10 +346,10 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case t => foldOver(c, t) - def checkParameters() = + def checkParams(refsToCheck: Refs, descr: => String) = val badParams = mutable.ListBuffer[Symbol]() def currentOwner = kind.dclSym.orElse(ctx.owner) - for hiddenRef <- prune(tpe.deepCaptureSet.elems.hidden.footprint) do + for hiddenRef <- prune(refsToCheck.footprint) do val refSym = hiddenRef.termSymbol if refSym.is(TermParam) && !refSym.hasAnnotation(defn.ConsumeAnnot) @@ -364,25 +364,29 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case p :: ps => i"${p.name}, ${paramsStr(ps)}" val (pluralS, singleS) = if badParams.tail.isEmpty then ("", "s") else ("s", "") report.error( - em"""Separation failure:$typeDescr type $tpe hides parameter$pluralS ${paramsStr(badParams.toList)} + em"""Separation failure: $descr parameter$pluralS ${paramsStr(badParams.toList)}. |The parameter$pluralS need$singleS to be annotated with @consume to allow this.""", pos) - def flagHiddenParams = - kind match - case TypeKind.Result(sym, _) => - !sym.isAnonymousFunction // we don't check return types of anonymous functions - && !sym.is(Case) // We don't check so far binders in patterns since they - // have inferred universal types. TODO come back to this; - // either infer more precise types for such binders or - // "see through them" when we look at hidden sets. - case TypeKind.Argument => - false + def checkParameters() = kind match + case TypeKind.Result(sym, _) => + if !sym.isAnonymousFunction // we don't check return types of anonymous functions + && !sym.is(Case) // We don't check so far binders in patterns since they + // have inferred universal types. TODO come back to this; + // either infer more precise types for such binders or + // "see through them" when we look at hidden sets. + then checkParams(tpe.deepCaptureSet.elems.hidden, i"$typeDescr type $tpe hides") + case TypeKind.Argument(arg) => + if tpe.hasAnnotation(defn.ConsumeAnnot) then + val capts = captures(arg) + def descr(verb: String) = i"argument to @consume parameter with type ${arg.nuType} $verb" + checkParams(capts, descr("refers to")) + checkParams(capts.hidden, descr("hides")) if !tpe.hasAnnotation(defn.UntrackedCapturesAnnot) then traverse(Captures.None, tpe) traverse.toCheck.foreach(checkParts) - if flagHiddenParams then checkParameters() + checkParameters() end checkType private def collectMethodTypes(tp: Type): List[TermLambda] = tp match diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index c6aef7225d16..8a6bbd43893e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -528,7 +528,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => traverseChildren(tree) postProcess(tree) - checkProperUse(tree) + checkProperUseOrConsume(tree) end traverse /** Processing done on node `tree` after its children are traversed */ @@ -682,16 +682,22 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => end postProcess - /** Check that @use annotations only appear on parameters and not on anonymous function parameters */ - def checkProperUse(tree: Tree)(using Context): Unit = tree match + /** Check that @use and @consume annotations only appear on parameters and not on + * anonymous function parameters + */ + def checkProperUseOrConsume(tree: Tree)(using Context): Unit = tree match case tree: MemberDef => - def useAllowed(sym: Symbol) = - (sym.is(Param) || sym.is(ParamAccessor)) && !sym.owner.isAnonymousFunction for ann <- tree.symbol.annotations do - if ann.symbol == defn.UseAnnot && !useAllowed(tree.symbol) then - report.error(i"Only parameters of methods can have @use annotations", tree.srcPos) + def isAllowedFor(sym: Symbol) = + (sym.is(Param) || sym.is(ParamAccessor)) + && (ann.symbol != defn.ConsumeAnnot || sym.isTerm) + && !sym.owner.isAnonymousFunction + def termStr = + if ann.symbol == defn.ConsumeAnnot then " term" else "" + if defn.ccParamOnlyAnnotations.contains(ann.symbol) && !isAllowedFor(tree.symbol) then + report.error(i"Only$termStr parameters of methods can have @${ann.symbol.name} annotations", tree.srcPos) case _ => - end checkProperUse + end checkProperUseOrConsume end setupTraverser // --------------- Adding capture set variables ---------------------------------- diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3fba3dbda082..d48ee2c0449e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1118,6 +1118,8 @@ class Definitions { @tu lazy val SilentAnnots: Set[Symbol] = Set(InlineParamAnnot, ErasedParamAnnot, RefineOverrideAnnot) + @tu lazy val ccParamOnlyAnnotations: Set[Symbol] = Set(UseAnnot, ConsumeAnnot) + // A list of annotations that are commonly used to indicate that a field/method argument or return // type is not null. These annotations are used by the nullification logic in JavaNullInterop to // improve the precision of type nullification. diff --git a/tests/neg-custom-args/captures/bad-uses-2.scala b/tests/neg-custom-args/captures/bad-uses-2.scala index 8dd121b2b134..7239c81b2f9a 100644 --- a/tests/neg-custom-args/captures/bad-uses-2.scala +++ b/tests/neg-custom-args/captures/bad-uses-2.scala @@ -1,7 +1,13 @@ -import caps.use -class Test: +import caps.{use, consume} +class TestUse: @use def F = ??? // error @use val x = ??? // error @use type T // error def foo[@use T](@use c: T): Unit = ??? // OK +class TestConsume: + @consume def F = ??? // error + @consume val x = ??? // error + @consume type T // error + def foo[@consume T](@use c: T): Unit = ??? // error + diff --git a/tests/neg-custom-args/captures/capt-depfun.check b/tests/neg-custom-args/captures/capt-depfun.check index 1faec0974a78..7cd838d72dc0 100644 --- a/tests/neg-custom-args/captures/capt-depfun.check +++ b/tests/neg-custom-args/captures/capt-depfun.check @@ -8,5 +8,5 @@ -- Error: tests/neg-custom-args/captures/capt-depfun.scala:11:24 ------------------------------------------------------- 11 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error: separatioon | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Separation failure: value dc's type Str^{y, z} => Str^{y, z} hides parameters y and z + | Separation failure: value dc's type Str^{y, z} => Str^{y, z} hides parameters y and z. | The parameters need to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index 2de01388def7..fc0e6c237647 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -15,5 +15,5 @@ -- Error: tests/neg-custom-args/captures/depfun-reach.scala:12:17 ------------------------------------------------------ 12 | : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - |Separation failure: method foo's result type (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] hides parameter op + |Separation failure: method foo's result type (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] hides parameter op. |The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 8b6534ce2c6b..5e5ae68cd29d 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -42,5 +42,5 @@ -- Error: tests/neg-custom-args/captures/i15772.scala:34:10 ------------------------------------------------------------ 34 | def c : C^ = new C(x) // error separation | ^^ - | Separation failure: method c's result type C^ hides parameter x + | Separation failure: method c's result type C^ hides parameter x. | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index 5ca8811043a3..894dd53bd303 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -13,5 +13,5 @@ -- Error: tests/neg-custom-args/captures/i19330.scala:16:14 ------------------------------------------------------------ 16 | val t: () => Logger^ = () => l // error | ^^^^^^^^^^^^^ - | Separation failure: value t's type () => (ex$5: caps.Exists) -> Logger^{ex$5} hides parameter l + | Separation failure: value t's type () => (ex$5: caps.Exists) -> Logger^{ex$5} hides parameter l. | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i21442.check b/tests/neg-custom-args/captures/i21442.check index 72b2170dcba4..1f8fbf4190d9 100644 --- a/tests/neg-custom-args/captures/i21442.check +++ b/tests/neg-custom-args/captures/i21442.check @@ -10,5 +10,5 @@ -- Error: tests/neg-custom-args/captures/i21442.scala:17:10 ------------------------------------------------------------ 17 | val x1: Boxed[IO^] = x // error | ^^^^^^^^^^ - | Separation failure: value x1's type Boxed[box IO^] hides parameter x + | Separation failure: value x1's type Boxed[box IO^] hides parameter x. | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check index 89e91dad314b..45d3553a77d3 100644 --- a/tests/neg-custom-args/captures/sepchecks2.check +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -36,17 +36,17 @@ -- Error: tests/neg-custom-args/captures/sepchecks2.scala:27:6 --------------------------------------------------------- 27 | bar((c, c)) // error | ^^^^^^ - | Separation failure in the argument's adapted type type (box Object^, box Object^). + | Separation failure in the argument's adapted type (box Object^, box Object^). | One part, box Object^ , hides {c}. | A previous part, box Object^ , also hides {c}. | The two sets overlap at {c}. -- Error: tests/neg-custom-args/captures/sepchecks2.scala:30:9 --------------------------------------------------------- 30 | val x: (Object^, Object^{c}) = (d, c) // error | ^^^^^^^^^^^^^^^^^^^^^ - | Separation failure: value x's type (box Object^, box Object^{c}) hides parameter d + | Separation failure: value x's type (box Object^, box Object^{c}) hides parameter d. | The parameter needs to be annotated with @consume to allow this. -- Error: tests/neg-custom-args/captures/sepchecks2.scala:33:9 --------------------------------------------------------- 33 | val x: (Object^, Object^) = (c, d) // error | ^^^^^^^^^^^^^^^^^^ - | Separation failure: value x's type (box Object^, box Object^) hides parameters c and d + | Separation failure: value x's type (box Object^, box Object^) hides parameters c and d. | The parameters need to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/sepchecks4.check b/tests/neg-custom-args/captures/sepchecks4.check index ddf001719b95..28f7ec55b944 100644 --- a/tests/neg-custom-args/captures/sepchecks4.check +++ b/tests/neg-custom-args/captures/sepchecks4.check @@ -1,12 +1,12 @@ -- Error: tests/neg-custom-args/captures/sepchecks4.scala:8:12 --------------------------------------------------------- 8 | val x: () => Unit = () => println(io) // error | ^^^^^^^^^^ - | Separation failure: value x's type () => Unit hides parameter io + | Separation failure: value x's type () => Unit hides parameter io. | The parameter needs to be annotated with @consume to allow this. -- Error: tests/neg-custom-args/captures/sepchecks4.scala:7:25 --------------------------------------------------------- 7 |def bad(io: Object^): () => Unit = // error | ^^^^^^^^^^ - | Separation failure: method bad's result type () => Unit hides parameter io + | Separation failure: method bad's result type () => Unit hides parameter io. | The parameter needs to be annotated with @consume to allow this. -- Error: tests/neg-custom-args/captures/sepchecks4.scala:12:25 -------------------------------------------------------- 12 | par(() => println(io))(() => println(io)) // error // (1) diff --git a/tests/neg-custom-args/captures/sepchecks5.check b/tests/neg-custom-args/captures/sepchecks5.check new file mode 100644 index 000000000000..b65b6a46e6e2 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks5.check @@ -0,0 +1,10 @@ +-- Error: tests/neg-custom-args/captures/sepchecks5.scala:12:37 -------------------------------------------------------- +12 |def bad(io: Object^): () => Unit = f(io) // error + | ^^ + | Separation failure: argument to @consume parameter with type (io : Object^) refers to parameter io. + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks5.scala:19:13 -------------------------------------------------------- +19 | val f2 = g(io) // error + | ^^ + | Separation failure: argument to @consume parameter with type (io : Object^) refers to parameter io. + | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/sepchecks5.scala b/tests/neg-custom-args/captures/sepchecks5.scala new file mode 100644 index 000000000000..5e2d4796f9f7 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks5.scala @@ -0,0 +1,21 @@ +import caps.{cap, consume} +import language.future +import language.experimental.captureChecking + +def par(op1: () => Unit)(op2: () => Unit): Unit = () + +def f(@consume io: Object^): () => Unit = + () => println(io) + +def g(@consume io: Object^): () => Unit = f(io) // ok + +def bad(io: Object^): () => Unit = f(io) // error + +def test(io: Object^): Unit = + + val f1 = bad(io) + par(f1)(() => println(io)) // !!! separation failure + + val f2 = g(io) // error + par(f2)(() => println(io)) // !!! separation failure + diff --git a/tests/neg-custom-args/captures/unsound-reach-6.check b/tests/neg-custom-args/captures/unsound-reach-6.check new file mode 100644 index 000000000000..4117508de818 --- /dev/null +++ b/tests/neg-custom-args/captures/unsound-reach-6.check @@ -0,0 +1,15 @@ +-- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:7:13 ---------------------------------------------------- +7 | println(xs.head) // error + | ^^^^^^^ + | Local reach capability xs* leaks into capture scope of method f. + | To allow this, the parameter xs should be declared with a @use annotation +-- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:11:14 --------------------------------------------------- +11 | val z = f(ys) // error @consume failure + | ^^ + |Separation failure: argument to @consume parameter with type (ys : List[box () ->{io} Unit]) refers to parameters ys and io. + |The parameters need to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:19:14 --------------------------------------------------- +19 | val z = f(ys) // error @consume failure + | ^^ + |Separation failure: argument to @consume parameter with type (ys : -> List[box () ->{io} Unit]) refers to parameter io. + |The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/unsound-reach-6.scala b/tests/neg-custom-args/captures/unsound-reach-6.scala index 5e73407eb94a..57a9f62496fb 100644 --- a/tests/neg-custom-args/captures/unsound-reach-6.scala +++ b/tests/neg-custom-args/captures/unsound-reach-6.scala @@ -8,7 +8,7 @@ def f(@consume xs: List[() => Unit]): () => Unit = () => def test(io: IO^)(ys: List[() ->{io} Unit]) = val x = () => - val z = f(ys) + val z = f(ys) // error @consume failure z() val _: () -> Unit = x // !!! ys* gets lost () @@ -16,7 +16,7 @@ def test(io: IO^)(ys: List[() ->{io} Unit]) = def test(io: IO^) = def ys: List[() ->{io} Unit] = ??? val x = () => - val z = f(ys) + val z = f(ys) // error @consume failure z() val _: () -> Unit = x // !!! io gets lost () From a4df033cb1fc86687a5da5aba11a821782ff5e6c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Jan 2025 15:49:17 +0100 Subject: [PATCH 14/93] Check that SharedCapabilities don't capture `cap`. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 25 +++++++++++++------ .../captures/shared-capability.check | 4 +++ .../captures/shared-capability.scala | 10 ++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 tests/neg-custom-args/captures/shared-capability.check create mode 100644 tests/neg-custom-args/captures/shared-capability.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 8a6bbd43893e..aa85a315ecef 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -85,7 +85,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Drops `private` from the flags of `symd` provided it is * a parameter accessor that's not `constructorOnly` or `uncheckedCaptured` * and that contains at least one @retains in co- or in-variant position. - * The @retains mught be implicit for a type deriving from `Capability`. + * The @retains might be implicit for a type deriving from `Capability`. */ private def newFlagsFor(symd: SymDenotation)(using Context): FlagSet = @@ -303,6 +303,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 6. Perform normalizeCaptures */ private def transformExplicitType(tp: Type, tptToCheck: Tree = EmptyTree)(using Context): Type = + + def fail(msg: Message) = + if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) + val toCapturing = new DeepTypeMap with FollowAliasesMap: override def toString = "expand aliases" @@ -332,7 +336,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else fntpe /** If C derives from Capability and we have a C^cs in source, we leave it as is - * instead of expanding it to C^{cap}^cs. We do this by stripping capability-generated + * instead of expanding it to C^{cap.rd}^cs. We do this by stripping capability-generated * universal capture sets from the parent of a CapturingType. */ def stripImpliedCaptureSet(tp: Type): Type = tp match @@ -341,10 +345,19 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: parent case _ => tp + def checkSharedOK(tp: Type): tp.type = + tp match + case CapturingType(parent, refs) + if refs.isUniversal && parent.derivesFrom(defn.Caps_SharedCapability) => + fail(em"$tp extends SharedCapability, so it cannot capture `cap`") + case _ => + tp + def apply(t: Type) = t match case t @ CapturingType(parent, refs) => - t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs) + checkSharedOK: + t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs) case t @ AnnotatedType(parent, ann) => val parent1 = this(parent) if ann.symbol.isRetains then @@ -352,7 +365,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if !tptToCheck.isEmpty then checkWellformedLater(parent2, ann.tree, tptToCheck) try - CapturingType(parent2, ann.tree.toCaptureSet) + checkSharedOK: + CapturingType(parent2, ann.tree.toCaptureSet) catch case ex: IllegalCaptureRef => report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) parent2 @@ -369,9 +383,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else normalizeCaptures(mapFollowingAliases(t)) end toCapturing - def fail(msg: Message) = - if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) - val tp1 = toCapturing(tp) val tp2 = Existential.mapCapInResults(fail)(tp1) if tp2 ne tp then capt.println(i"expanded explicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") diff --git a/tests/neg-custom-args/captures/shared-capability.check b/tests/neg-custom-args/captures/shared-capability.check new file mode 100644 index 000000000000..64fb3eb39d44 --- /dev/null +++ b/tests/neg-custom-args/captures/shared-capability.check @@ -0,0 +1,4 @@ +-- Error: tests/neg-custom-args/captures/shared-capability.scala:9:13 -------------------------------------------------- +9 |def test2(a: Async^): Object^ = a // error + | ^^^^^^ + | Async^ extends SharedCapability, so it cannot capture `cap` diff --git a/tests/neg-custom-args/captures/shared-capability.scala b/tests/neg-custom-args/captures/shared-capability.scala new file mode 100644 index 000000000000..23af2ff177e8 --- /dev/null +++ b/tests/neg-custom-args/captures/shared-capability.scala @@ -0,0 +1,10 @@ + +import language.future // sepchecks on +import caps.SharedCapability + +class Async extends SharedCapability + +def test1(a: Async): Object^ = a // OK + +def test2(a: Async^): Object^ = a // error + From 52dc9b497fca6d5316fe48bb08ffa58f070690bf Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Jan 2025 16:59:13 +0100 Subject: [PATCH 15/93] Turn separation checking on by default Downgrade to -source 3.6 to turn it off. --- project/Build.scala | 2 +- tests/neg-custom-args/captures/box-adapt-cases.scala | 2 +- tests/neg-custom-args/captures/box-adapt-contra.scala | 2 +- tests/neg-custom-args/captures/capt-depfun.scala | 2 +- tests/neg-custom-args/captures/capt1.scala | 2 +- tests/neg-custom-args/captures/cc-ex-conformance.scala | 2 +- tests/neg-custom-args/captures/delayedRunops.scala | 2 +- tests/neg-custom-args/captures/depfun-reach.scala | 2 +- tests/neg-custom-args/captures/effect-swaps-explicit.scala | 2 +- tests/neg-custom-args/captures/effect-swaps.scala | 2 +- tests/neg-custom-args/captures/existential-mapping.scala | 2 +- tests/neg-custom-args/captures/filevar-expanded.scala | 2 +- tests/neg-custom-args/captures/i15772.scala | 2 +- tests/neg-custom-args/captures/i19330.scala | 2 +- tests/neg-custom-args/captures/i21442.scala | 2 +- tests/neg-custom-args/captures/i21614.scala | 2 +- tests/neg-custom-args/captures/i22005.scala | 2 +- tests/neg-custom-args/captures/lazyref.scala | 2 +- tests/neg-custom-args/captures/outer-var.scala | 2 +- tests/neg-custom-args/captures/reaches.scala | 2 +- tests/neg-custom-args/captures/reaches2.scala | 2 +- tests/neg-custom-args/captures/sep-compose.scala | 2 +- tests/neg-custom-args/captures/sep-use.scala | 2 +- tests/neg-custom-args/captures/sep-use2.scala | 2 +- tests/neg-custom-args/captures/sepchecks.scala | 2 +- tests/neg-custom-args/captures/sepchecks2.scala | 2 +- tests/neg-custom-args/captures/sepchecks3.scala | 2 +- tests/neg-custom-args/captures/shared-capability.scala | 2 +- tests/neg-custom-args/captures/unsound-reach-2.scala | 2 +- tests/neg-custom-args/captures/unsound-reach-3.scala | 2 +- tests/neg-custom-args/captures/unsound-reach-4.scala | 2 +- tests/neg-custom-args/captures/unsound-reach-6.scala | 2 +- tests/neg-custom-args/captures/unsound-reach.scala | 2 +- tests/neg-custom-args/captures/vars.scala | 2 +- tests/pos-custom-args/captures/capt1.scala | 2 +- tests/pos-custom-args/captures/cc-this.scala | 2 +- tests/pos-custom-args/captures/i15749a.scala | 2 +- tests/pos-custom-args/captures/lazyref.scala | 2 +- tests/pos-custom-args/captures/reaches.scala | 2 +- tests/pos-custom-args/captures/sep-compose.scala | 2 +- tests/pos-custom-args/captures/sep-eq.scala | 2 +- tests/pos-custom-args/captures/skolems2.scala | 2 +- 42 files changed, 42 insertions(+), 42 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 5d68f478250e..6c0d8454ee01 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1246,7 +1246,7 @@ object Build { settings(scala2LibraryBootstrappedSettings). settings( moduleName := "scala2-library-cc", - scalacOptions ++= Seq("-Ycheck:all", "-source", "3.7") + scalacOptions += "-Ycheck:all" ) lazy val scala2LibraryBootstrappedSettings = Seq( diff --git a/tests/neg-custom-args/captures/box-adapt-cases.scala b/tests/neg-custom-args/captures/box-adapt-cases.scala index 55371c4e50b7..7db58318ed05 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.scala +++ b/tests/neg-custom-args/captures/box-adapt-cases.scala @@ -1,4 +1,4 @@ -import language.`3.7` // sepchecks on + trait Cap { def use(): Int } def test1(): Unit = { diff --git a/tests/neg-custom-args/captures/box-adapt-contra.scala b/tests/neg-custom-args/captures/box-adapt-contra.scala index e74766228e45..95affbe2aef9 100644 --- a/tests/neg-custom-args/captures/box-adapt-contra.scala +++ b/tests/neg-custom-args/captures/box-adapt-contra.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.consume trait Cap diff --git a/tests/neg-custom-args/captures/capt-depfun.scala b/tests/neg-custom-args/captures/capt-depfun.scala index f55f396cf681..384c403bdd27 100644 --- a/tests/neg-custom-args/captures/capt-depfun.scala +++ b/tests/neg-custom-args/captures/capt-depfun.scala @@ -1,5 +1,5 @@ import annotation.retains -import language.future // sepchecks on + class C type Cap = C @retains(caps.cap) diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala index 687073c3cdae..48778dbd6716 100644 --- a/tests/neg-custom-args/captures/capt1.scala +++ b/tests/neg-custom-args/captures/capt1.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import annotation.retains class C def f(x: C @retains(caps.cap), y: C): () -> C = diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.scala b/tests/neg-custom-args/captures/cc-ex-conformance.scala index 4920f26ac380..3b685c5f76b1 100644 --- a/tests/neg-custom-args/captures/cc-ex-conformance.scala +++ b/tests/neg-custom-args/captures/cc-ex-conformance.scala @@ -1,6 +1,6 @@ import language.experimental.captureChecking import caps.{Exists, Capability} -import language.future // sepchecks on + class C diff --git a/tests/neg-custom-args/captures/delayedRunops.scala b/tests/neg-custom-args/captures/delayedRunops.scala index 1eaf6a0b3efe..878bdb68687f 100644 --- a/tests/neg-custom-args/captures/delayedRunops.scala +++ b/tests/neg-custom-args/captures/delayedRunops.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking -import language.future // sepchecks on + import caps.{use, consume} // ok diff --git a/tests/neg-custom-args/captures/depfun-reach.scala b/tests/neg-custom-args/captures/depfun-reach.scala index e7affafe0240..4b2d662901b7 100644 --- a/tests/neg-custom-args/captures/depfun-reach.scala +++ b/tests/neg-custom-args/captures/depfun-reach.scala @@ -1,6 +1,6 @@ import language.experimental.captureChecking import caps.cap -import language.`3.7` // sepchecks on + def test(io: Object^, async: Object^) = def compose(op: List[(() ->{cap} Unit, () ->{cap} Unit)]): List[() ->{op*} Unit] = List(() => op.foreach((f,g) => { f(); g() })) diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.scala b/tests/neg-custom-args/captures/effect-swaps-explicit.scala index 784c403d25fa..b3756056abbd 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.scala +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + object boundary: diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index 40ddd8917b2b..3f0cc25fbb25 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + object boundary: diff --git a/tests/neg-custom-args/captures/existential-mapping.scala b/tests/neg-custom-args/captures/existential-mapping.scala index aa45e60cdabc..290f7dc767a6 100644 --- a/tests/neg-custom-args/captures/existential-mapping.scala +++ b/tests/neg-custom-args/captures/existential-mapping.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking -import language.`3.7` // sepchecks on + class A class C type Fun[X] = (x: C^) -> X diff --git a/tests/neg-custom-args/captures/filevar-expanded.scala b/tests/neg-custom-args/captures/filevar-expanded.scala index c42f9478256f..461a617bde0d 100644 --- a/tests/neg-custom-args/captures/filevar-expanded.scala +++ b/tests/neg-custom-args/captures/filevar-expanded.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import language.experimental.modularity import compiletime.uninitialized -import language.future // sepchecks on + object test1: class File: def write(x: String): Unit = ??? diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index 25dc2bbe1c2b..face1e8a0ff5 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + type Observe[T] = (T => Unit) -> Unit diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index bbc0b8483367..9c589792a9e3 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -1,4 +1,4 @@ -import language.`3.7` // sepchecks on + import language.experimental.captureChecking diff --git a/tests/neg-custom-args/captures/i21442.scala b/tests/neg-custom-args/captures/i21442.scala index 8cfa0122cd1b..3541bd89789a 100644 --- a/tests/neg-custom-args/captures/i21442.scala +++ b/tests/neg-custom-args/captures/i21442.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking -import language.future // sepchecks on + trait IO: def use(): Unit case class Boxed[+T](unbox: T) diff --git a/tests/neg-custom-args/captures/i21614.scala b/tests/neg-custom-args/captures/i21614.scala index d21fb2f5d3a0..f5bab90f543b 100644 --- a/tests/neg-custom-args/captures/i21614.scala +++ b/tests/neg-custom-args/captures/i21614.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import caps.Capability import caps.use -import language.`3.7` // sepchecks on + trait List[+T]: def map[U](f: T => U): List[U] diff --git a/tests/neg-custom-args/captures/i22005.scala b/tests/neg-custom-args/captures/i22005.scala index da8b9f6bc95c..689246d6f835 100644 --- a/tests/neg-custom-args/captures/i22005.scala +++ b/tests/neg-custom-args/captures/i22005.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.* class IO diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 230b93edfea7..549bcc2257a5 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + class CC type Cap = CC^ diff --git a/tests/neg-custom-args/captures/outer-var.scala b/tests/neg-custom-args/captures/outer-var.scala index 4ec19d8f8971..eb82312d4b37 100644 --- a/tests/neg-custom-args/captures/outer-var.scala +++ b/tests/neg-custom-args/captures/outer-var.scala @@ -1,4 +1,4 @@ -import language.`3.7` // sepchecks on + class CC type Cap = CC^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index 712069c7970d..d4f9ceee3de2 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,4 +1,4 @@ -import caps.use; import language.`3.7` // sepchecks on +import caps.use; import caps.consume class File: def write(): Unit = ??? diff --git a/tests/neg-custom-args/captures/reaches2.scala b/tests/neg-custom-args/captures/reaches2.scala index 9620d57d42e6..69ee3472cf86 100644 --- a/tests/neg-custom-args/captures/reaches2.scala +++ b/tests/neg-custom-args/captures/reaches2.scala @@ -1,4 +1,4 @@ -import language.`3.8` // sepchecks on + class List[+A]: def map[B](f: A -> B): List[B] = ??? diff --git a/tests/neg-custom-args/captures/sep-compose.scala b/tests/neg-custom-args/captures/sep-compose.scala index 268076cd40aa..cfa3318b315f 100644 --- a/tests/neg-custom-args/captures/sep-compose.scala +++ b/tests/neg-custom-args/captures/sep-compose.scala @@ -1,5 +1,5 @@ import caps.cap -import language.future // sepchecks on + def seq1(x: () => Unit, y: () ->{x, cap} Unit): Unit = x(); y() diff --git a/tests/neg-custom-args/captures/sep-use.scala b/tests/neg-custom-args/captures/sep-use.scala index 53f3fddd18a7..f83160ad075c 100644 --- a/tests/neg-custom-args/captures/sep-use.scala +++ b/tests/neg-custom-args/captures/sep-use.scala @@ -1,5 +1,5 @@ import caps.{cap, consume} -import language.future // sepchecks on + def test1(@consume io: Object^): Unit = diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala index 545b9d77efde..a1f2740df9fa 100644 --- a/tests/neg-custom-args/captures/sep-use2.scala +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.consume def test1(@consume c: Object^, f: Object^ => Object^) = diff --git a/tests/neg-custom-args/captures/sepchecks.scala b/tests/neg-custom-args/captures/sepchecks.scala index ceb6ce7b30bb..4508b6839781 100644 --- a/tests/neg-custom-args/captures/sepchecks.scala +++ b/tests/neg-custom-args/captures/sepchecks.scala @@ -1,6 +1,6 @@ import caps.Mutable import caps.cap -import language.future // sepchecks on + trait Rdr[T]: def get: T diff --git a/tests/neg-custom-args/captures/sepchecks2.scala b/tests/neg-custom-args/captures/sepchecks2.scala index a9b88649820d..34ca3a56a243 100644 --- a/tests/neg-custom-args/captures/sepchecks2.scala +++ b/tests/neg-custom-args/captures/sepchecks2.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.consume def foo(xs: List[() => Unit], y: Object^) = ??? diff --git a/tests/neg-custom-args/captures/sepchecks3.scala b/tests/neg-custom-args/captures/sepchecks3.scala index cdc52920bb55..8cc7d705b42b 100644 --- a/tests/neg-custom-args/captures/sepchecks3.scala +++ b/tests/neg-custom-args/captures/sepchecks3.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.consume def foo(xs: List[() => Unit], y: Object^) = ??? diff --git a/tests/neg-custom-args/captures/shared-capability.scala b/tests/neg-custom-args/captures/shared-capability.scala index 23af2ff177e8..262a6db386ba 100644 --- a/tests/neg-custom-args/captures/shared-capability.scala +++ b/tests/neg-custom-args/captures/shared-capability.scala @@ -1,5 +1,5 @@ -import language.future // sepchecks on + import caps.SharedCapability class Async extends SharedCapability diff --git a/tests/neg-custom-args/captures/unsound-reach-2.scala b/tests/neg-custom-args/captures/unsound-reach-2.scala index 90dd3824099f..944ef82da5bb 100644 --- a/tests/neg-custom-args/captures/unsound-reach-2.scala +++ b/tests/neg-custom-args/captures/unsound-reach-2.scala @@ -1,4 +1,4 @@ -import language.experimental.captureChecking; import language.`3.7` // sepchecks on +import language.experimental.captureChecking; trait Consumer[-T]: def apply(x: T): Unit diff --git a/tests/neg-custom-args/captures/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala index 052f757da3a3..0aeb10e39916 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import language.experimental.captureChecking import caps.consume diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index 108619a4780a..d636afd027d4 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -1,6 +1,6 @@ -import language.future // sepchecks on + import language.experimental.captureChecking; import caps.consume diff --git a/tests/neg-custom-args/captures/unsound-reach-6.scala b/tests/neg-custom-args/captures/unsound-reach-6.scala index 57a9f62496fb..4ce789025837 100644 --- a/tests/neg-custom-args/captures/unsound-reach-6.scala +++ b/tests/neg-custom-args/captures/unsound-reach-6.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.consume class IO diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index fc8e2328ceb8..0aa7f1fc7ee9 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -1,4 +1,4 @@ -import language.experimental.captureChecking; import language.`3.7` // sepchecks on +import language.experimental.captureChecking; trait File: def close(): Unit diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index fc0de7354dd3..eb9719cd2adf 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -1,4 +1,4 @@ -import language.`3.7` // sepchecks on + class CC type Cap = CC^ diff --git a/tests/pos-custom-args/captures/capt1.scala b/tests/pos-custom-args/captures/capt1.scala index 34e9e40e7fdb..f8cf39933bef 100644 --- a/tests/pos-custom-args/captures/capt1.scala +++ b/tests/pos-custom-args/captures/capt1.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.unsafe.unsafeAssumeSeparate class C diff --git a/tests/pos-custom-args/captures/cc-this.scala b/tests/pos-custom-args/captures/cc-this.scala index 803470e270fa..638c20d94a91 100644 --- a/tests/pos-custom-args/captures/cc-this.scala +++ b/tests/pos-custom-args/captures/cc-this.scala @@ -1,5 +1,5 @@ import caps.consume -import language.future // sepchecks on + class Cap extends caps.Capability diff --git a/tests/pos-custom-args/captures/i15749a.scala b/tests/pos-custom-args/captures/i15749a.scala index d6f2d193dae2..c008d20a155c 100644 --- a/tests/pos-custom-args/captures/i15749a.scala +++ b/tests/pos-custom-args/captures/i15749a.scala @@ -1,6 +1,6 @@ import caps.cap import caps.use -import language.`3.7` // sepchecks on + class Unit object u extends Unit diff --git a/tests/pos-custom-args/captures/lazyref.scala b/tests/pos-custom-args/captures/lazyref.scala index ba31e0f6e908..f4c85de9a51a 100644 --- a/tests/pos-custom-args/captures/lazyref.scala +++ b/tests/pos-custom-args/captures/lazyref.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.consume class Cap extends caps.Capability diff --git a/tests/pos-custom-args/captures/reaches.scala b/tests/pos-custom-args/captures/reaches.scala index aad86ecbc3c4..131dce862b02 100644 --- a/tests/pos-custom-args/captures/reaches.scala +++ b/tests/pos-custom-args/captures/reaches.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.{use, consume} class C diff --git a/tests/pos-custom-args/captures/sep-compose.scala b/tests/pos-custom-args/captures/sep-compose.scala index 3f6ef2968a6e..3a56bd789282 100644 --- a/tests/pos-custom-args/captures/sep-compose.scala +++ b/tests/pos-custom-args/captures/sep-compose.scala @@ -1,5 +1,5 @@ import caps.cap -import language.future // sepchecks on + def seq1(x: () => Unit, y: () ->{x, cap} Unit): Unit = x(); y() diff --git a/tests/pos-custom-args/captures/sep-eq.scala b/tests/pos-custom-args/captures/sep-eq.scala index 836633feee9e..94a64cc0c8f4 100644 --- a/tests/pos-custom-args/captures/sep-eq.scala +++ b/tests/pos-custom-args/captures/sep-eq.scala @@ -1,6 +1,6 @@ import caps.Mutable import caps.cap -import language.future // sepchecks on + extension (x: Object^) infix def eql (y: Object^{x, cap}): Boolean = x eq y diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala index a891ad46616c..76fb6b371587 100644 --- a/tests/pos-custom-args/captures/skolems2.scala +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -1,4 +1,4 @@ -import language.future // sepchecks on + import caps.consume def Test(@consume c: Object^, f: Object^ => Object^) = From 824dd835e5ae83f277675278a86b8d9528bd6974 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 27 Jan 2025 18:55:33 +0100 Subject: [PATCH 16/93] Make sure fresh results of methods only hide local refs --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../src/dotty/tools/dotc/cc/SepCheck.scala | 45 ++++++++++--------- .../captures/delayedRunops.check | 4 ++ .../captures/delayedRunops.scala | 2 +- tests/neg-custom-args/captures/i15772.check | 3 +- .../captures/non-local-consume.scala | 23 ++++++++++ tests/neg-custom-args/captures/sep-use.check | 12 +++++ tests/neg-custom-args/captures/sep-use.scala | 6 +-- tests/neg-custom-args/captures/sep-use2.scala | 4 +- .../captures/unsound-reach-6.check | 6 +-- .../captures/eta-expansions.scala | 2 +- tests/pos-custom-args/captures/skolems2.scala | 3 +- 12 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 tests/neg-custom-args/captures/non-local-consume.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index d64f3e81603a..2278ee5d1f2c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -281,7 +281,7 @@ extension (tp: Type) /** The first element of this path type */ final def pathRoot(using Context): Type = tp.dealias match - case tp1: NamedType if tp1.symbol.owner.isClass => tp1.prefix.pathRoot + case tp1: NamedType if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot case tp1 => tp1 /** If this part starts with `C.this`, the class `C`. diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 7fe50eec02a6..43d55476b380 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -346,17 +346,19 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case t => foldOver(c, t) - def checkParams(refsToCheck: Refs, descr: => String) = + def checkRefs(refsToCheck: Refs, descr: => String) = val badParams = mutable.ListBuffer[Symbol]() def currentOwner = kind.dclSym.orElse(ctx.owner) - for hiddenRef <- prune(refsToCheck.footprint) do - val refSym = hiddenRef.termSymbol - if refSym.is(TermParam) - && !refSym.hasAnnotation(defn.ConsumeAnnot) - && !refSym.info.derivesFrom(defn.Caps_SharedCapability) - && currentOwner.isContainedIn(refSym.owner) - then - badParams += refSym + for hiddenRef <- prune(refsToCheck) do + val refSym = hiddenRef.pathRoot.termSymbol // TODO also hangle ThisTypes as pathRoots + if refSym.exists && !refSym.info.derivesFrom(defn.Caps_SharedCapability) then + if currentOwner.enclosingMethodOrClass.isProperlyContainedIn(refSym.owner.enclosingMethodOrClass) then + report.error(em"""Separation failure: $descr non-local $refSym""", pos) + else if refSym.is(TermParam) + && !refSym.hasAnnotation(defn.ConsumeAnnot) + && currentOwner.isContainedIn(refSym.owner) + then + badParams += refSym if badParams.nonEmpty then def paramsStr(params: List[Symbol]): String = (params: @unchecked) match case p :: Nil => i"${p.name}" @@ -368,25 +370,28 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: |The parameter$pluralS need$singleS to be annotated with @consume to allow this.""", pos) - def checkParameters() = kind match + def checkLegalRefs() = kind match case TypeKind.Result(sym, _) => if !sym.isAnonymousFunction // we don't check return types of anonymous functions && !sym.is(Case) // We don't check so far binders in patterns since they // have inferred universal types. TODO come back to this; // either infer more precise types for such binders or // "see through them" when we look at hidden sets. - then checkParams(tpe.deepCaptureSet.elems.hidden, i"$typeDescr type $tpe hides") + then + val refs = tpe.deepCaptureSet.elems + val toCheck = refs.hidden.footprint -- refs.footprint + checkRefs(toCheck, i"$typeDescr type $tpe hides") case TypeKind.Argument(arg) => if tpe.hasAnnotation(defn.ConsumeAnnot) then val capts = captures(arg) def descr(verb: String) = i"argument to @consume parameter with type ${arg.nuType} $verb" - checkParams(capts, descr("refers to")) - checkParams(capts.hidden, descr("hides")) + checkRefs(capts.footprint, descr("refers to")) + checkRefs(capts.hidden.footprint, descr("hides")) if !tpe.hasAnnotation(defn.UntrackedCapturesAnnot) then traverse(Captures.None, tpe) traverse.toCheck.foreach(checkParts) - checkParameters() + checkLegalRefs() end checkType private def collectMethodTypes(tp: Type): List[TermLambda] = tp match @@ -426,10 +431,12 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if argss.nestedExists(_.needsSepCheck) then checkApply(tree, argss.flatten, dependencies(tree, argss)) + def isUnsafeAssumeSeparate(tree: Tree)(using Context): Boolean = tree match + case tree: Apply => tree.symbol == defn.Caps_unsafeAssumeSeparate + case _ => false + def traverse(tree: Tree)(using Context): Unit = - tree match - case tree: Apply if tree.symbol == defn.Caps_unsafeAssumeSeparate => return - case _ => + if isUnsafeAssumeSeparate(tree) then return checkUse(tree) tree match case tree: GenericApply => @@ -446,7 +453,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: defsShadow = saved case tree: ValOrDefDef => traverseChildren(tree) - if !tree.symbol.isOneOf(TermParamOrAccessor) then + if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then checkType(tree.tpt, tree.symbol) if previousDefs.nonEmpty then capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hidden.footprint}") @@ -460,5 +467,3 @@ end SepChecker - - diff --git a/tests/neg-custom-args/captures/delayedRunops.check b/tests/neg-custom-args/captures/delayedRunops.check index 624dd1eaf079..372e010d191a 100644 --- a/tests/neg-custom-args/captures/delayedRunops.check +++ b/tests/neg-custom-args/captures/delayedRunops.check @@ -12,3 +12,7 @@ | ^^^^ | reference ops* is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Unit +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:22:16 ----------------------------------------------------- +22 | val ops1: List[() => Unit] = ops // error + | ^^^^^^^^^^^^^^^^ + | Separation failure: value ops1's type List[box () => Unit] hides non-local parameter ops diff --git a/tests/neg-custom-args/captures/delayedRunops.scala b/tests/neg-custom-args/captures/delayedRunops.scala index 878bdb68687f..1108be9c938a 100644 --- a/tests/neg-custom-args/captures/delayedRunops.scala +++ b/tests/neg-custom-args/captures/delayedRunops.scala @@ -19,7 +19,7 @@ import caps.{use, consume} // unsound: impure operation pretended pure def delayedRunOps2(@consume ops: List[() => Unit]): () ->{} Unit = () => - val ops1: List[() => Unit] = ops + val ops1: List[() => Unit] = ops // error runOps(ops1) // error // unsound: impure operation pretended pure diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 5e5ae68cd29d..f4d5b4d60189 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -42,5 +42,4 @@ -- Error: tests/neg-custom-args/captures/i15772.scala:34:10 ------------------------------------------------------------ 34 | def c : C^ = new C(x) // error separation | ^^ - | Separation failure: method c's result type C^ hides parameter x. - | The parameter needs to be annotated with @consume to allow this. + | Separation failure: method c's result type C^ hides non-local parameter x diff --git a/tests/neg-custom-args/captures/non-local-consume.scala b/tests/neg-custom-args/captures/non-local-consume.scala new file mode 100644 index 000000000000..cfdbe4a4aa0c --- /dev/null +++ b/tests/neg-custom-args/captures/non-local-consume.scala @@ -0,0 +1,23 @@ +import caps.{cap, consume, Mutable} +import language.experimental.captureChecking + +class Buffer extends Mutable + +def f1(@consume buf: Buffer^): Buffer^ = + val buf1: Buffer^ = buf // OK + buf1 + +def f2(@consume buf: Buffer^): Buffer^ = + def g(): Buffer^ = buf // error + g() + +def f3(@consume buf: Buffer^): Buffer^ = + val buf1 = buf + def g(): Buffer^ = buf1 // error + g() + +def f4(@consume buf: Buffer^): Buffer^ = + val buf1: Buffer^ = buf + def g(): Buffer^ = buf1 // error + g() + diff --git a/tests/neg-custom-args/captures/sep-use.check b/tests/neg-custom-args/captures/sep-use.check index 9379c29fc950..64e2bd7800bc 100644 --- a/tests/neg-custom-args/captures/sep-use.check +++ b/tests/neg-custom-args/captures/sep-use.check @@ -4,18 +4,30 @@ | Separation failure: Illegal access to {io} which is hidden by the previous definition | of value x with type () => Unit. | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:12:12 ----------------------------------------------------------- +12 | def x: () => Unit = () => println(io) // error + | ^^^^^^^^^^ + | Separation failure: method x's result type () => Unit hides non-local parameter io -- Error: tests/neg-custom-args/captures/sep-use.scala:13:10 ----------------------------------------------------------- 13 | println(io) // error | ^^ | Separation failure: Illegal access to {io} which is hidden by the previous definition | of method x with result type () => Unit. | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:18:10 ----------------------------------------------------------- +18 | def xx: (y: Int) => Unit = _ => println(io) // error + | ^^^^^^^^^^^^^^^^ + | Separation failure: method xx's result type (y: Int) => Unit hides non-local parameter io -- Error: tests/neg-custom-args/captures/sep-use.scala:19:10 ----------------------------------------------------------- 19 | println(io) // error | ^^ | Separation failure: Illegal access to {io} which is hidden by the previous definition | of method xx with result type (y: Int) => Unit. | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:24:19 ----------------------------------------------------------- +24 | def xxx(y: Int): Object^ = io // error + | ^^^^^^^ + | Separation failure: method xxx's result type Object^ hides non-local parameter io -- Error: tests/neg-custom-args/captures/sep-use.scala:25:10 ----------------------------------------------------------- 25 | println(io) // error | ^^ diff --git a/tests/neg-custom-args/captures/sep-use.scala b/tests/neg-custom-args/captures/sep-use.scala index f83160ad075c..e89adb0f060e 100644 --- a/tests/neg-custom-args/captures/sep-use.scala +++ b/tests/neg-custom-args/captures/sep-use.scala @@ -9,19 +9,19 @@ def test1(@consume io: Object^): Unit = def test2(@consume io: Object^): Unit = - def x: () => Unit = () => println(io) + def x: () => Unit = () => println(io) // error println(io) // error println(x) // ok def test3(@consume io: Object^): Unit = - def xx: (y: Int) => Unit = _ => println(io) + def xx: (y: Int) => Unit = _ => println(io) // error println(io) // error println(xx(2)) // ok def test4(@consume io: Object^): Unit = - def xxx(y: Int): Object^ = io + def xxx(y: Int): Object^ = io // error println(io) // error println(xxx(2)) // ok diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala index a1f2740df9fa..48f2a84c6fe4 100644 --- a/tests/neg-custom-args/captures/sep-use2.scala +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -2,7 +2,7 @@ import caps.consume def test1(@consume c: Object^, f: Object^ => Object^) = - def cc: Object^ = c + def cc: Object^ = c // error val x1 = { f(cc) } // ok val x2 = @@ -13,7 +13,7 @@ def test1(@consume c: Object^, f: Object^ => Object^) = { f(c) } // error def test2(@consume c: Object^, f: Object^ ->{c} Object^) = - def cc: Object^ = c + def cc: Object^ = c // error val x1 = { f(cc) } // error // error val x4: Object^ = diff --git a/tests/neg-custom-args/captures/unsound-reach-6.check b/tests/neg-custom-args/captures/unsound-reach-6.check index 4117508de818..90fd6c40fbfd 100644 --- a/tests/neg-custom-args/captures/unsound-reach-6.check +++ b/tests/neg-custom-args/captures/unsound-reach-6.check @@ -6,10 +6,8 @@ -- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:11:14 --------------------------------------------------- 11 | val z = f(ys) // error @consume failure | ^^ - |Separation failure: argument to @consume parameter with type (ys : List[box () ->{io} Unit]) refers to parameters ys and io. - |The parameters need to be annotated with @consume to allow this. + |Separation failure: argument to @consume parameter with type (ys : List[box () ->{io} Unit]) refers to non-local parameter ys -- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:19:14 --------------------------------------------------- 19 | val z = f(ys) // error @consume failure | ^^ - |Separation failure: argument to @consume parameter with type (ys : -> List[box () ->{io} Unit]) refers to parameter io. - |The parameter needs to be annotated with @consume to allow this. + |Separation failure: argument to @consume parameter with type (ys : -> List[box () ->{io} Unit]) refers to non-local parameter io diff --git a/tests/pos-custom-args/captures/eta-expansions.scala b/tests/pos-custom-args/captures/eta-expansions.scala index b4e38cdf0856..cbe72137bd65 100644 --- a/tests/pos-custom-args/captures/eta-expansions.scala +++ b/tests/pos-custom-args/captures/eta-expansions.scala @@ -3,7 +3,7 @@ class Cap extends caps.Capability def test(d: Cap) = def map2(xs: List[Int])(f: Int => Int): List[Int] = xs.map(f) val f1 = map2 // capture polymorphic implicit eta expansion - def f2c: List[Int] => (Int => Int) => List[Int] = f1 + val f2c: List[Int] => (Int => Int) => List[Int] = f1 val a0 = identity[Cap ->{d} Unit] // capture monomorphic implicit eta expansion val a0c: (Cap ->{d} Unit) ->{d} Cap ->{d} Unit = a0 val b0 = (x: Cap ->{d} Unit) => identity[Cap ->{d} Unit](x) // not an implicit eta expansion, hence capture polymorphic diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala index 76fb6b371587..74438aa5793a 100644 --- a/tests/pos-custom-args/captures/skolems2.scala +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -1,8 +1,9 @@ import caps.consume +import caps.unsafe.unsafeAssumeSeparate def Test(@consume c: Object^, f: Object^ => Object^) = - def cc: Object^ = c + def cc: Object^ = unsafeAssumeSeparate(c) val x1 = { f(cc) } val x2 = From 7b0b80b8098c52eebabe949f8b728323711c9b62 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Jan 2025 18:06:24 +0100 Subject: [PATCH 17/93] Make sure parameters are not used again after they are consumed --- .../src/dotty/tools/dotc/cc/SepCheck.scala | 181 ++++++++++++++++-- .../captures/linear-buffer.check | 29 +++ .../captures/linear-buffer.scala | 42 ++++ .../captures/non-local-consume.scala | 6 + 4 files changed, 239 insertions(+), 19 deletions(-) create mode 100644 tests/neg-custom-args/captures/linear-buffer.check create mode 100644 tests/neg-custom-args/captures/linear-buffer.scala diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 43d55476b380..47c394879255 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -12,6 +12,7 @@ import config.Printers.capt import StdNames.nme import util.{SimpleIdentitySet, EqHashMap, SrcPos} import tpd.* +import reflect.ClassTag object SepChecker: @@ -39,6 +40,84 @@ object SepChecker: case _ => NoSymbol end TypeKind + /** A class for segmented sets of consumed references. + * References are associated with the source positions where they first appeared. + * References are compared with `eq`. + */ + abstract class ConsumedSet: + /** The references in the set. The array should be treated as immutable in client code */ + def refs: Array[CaptureRef] + + /** The associated source positoons. The array should be treated as immutable in client code */ + def locs: Array[SrcPos] + + /** The number of references in the set */ + def size: Int + + def toMap: Map[CaptureRef, SrcPos] = refs.take(size).zip(locs).toMap + + def show(using Context) = + s"[${toMap.map((ref, loc) => i"$ref -> $loc").toList}]" + end ConsumedSet + + /** A fixed consumed set consisting of the given references `refs` and + * associated source positions `locs` + */ + class ConstConsumedSet(val refs: Array[CaptureRef], val locs: Array[SrcPos]) extends ConsumedSet: + def size = refs.size + + /** A mutable consumed set, which is initially empty */ + class MutConsumedSet extends ConsumedSet: + var refs: Array[CaptureRef] = new Array(4) + var locs: Array[SrcPos] = new Array(4) + var size = 0 + + private def double[T <: AnyRef : ClassTag](xs: Array[T]): Array[T] = + val xs1 = new Array[T](xs.length * 2) + xs.copyToArray(xs1) + xs1 + + private def ensureCapacity(added: Int): Unit = + if size + added > refs.length then + refs = double(refs) + locs = double(locs) + + /** If `ref` is in the set, its associated source position, otherwise `null` */ + def get(ref: CaptureRef): SrcPos | Null = + var i = 0 + while i < size && (refs(i) ne ref) do i += 1 + if i < size then locs(i) else null + + /** If `ref` is not yet in the set, add it with given source position */ + def put(ref: CaptureRef, loc: SrcPos): Unit = + if get(ref) == null then + ensureCapacity(1) + refs(size) = ref + locs(size) = loc + size += 1 + + /** Add all references with their associated positions from `that` which + * are not yet in the set. + */ + def ++= (that: ConsumedSet): Unit = + for i <- 0 until that.size do put(that.refs(i), that.locs(i)) + + /** Run `op` and return any new references it created in a separate `ConsumedSet`. + * The current mutable set is reset to its state before `op` was run. + */ + def segment(op: => Unit): ConsumedSet = + val start = size + try + op + if size == start then EmptyConsumedSet + else ConstConsumedSet(refs.slice(start, size), locs.slice(start, size)) + finally + size = start + + end MutConsumedSet + + val EmptyConsumedSet = ConstConsumedSet(Array(), Array()) + class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import checker.* import SepChecker.* @@ -46,7 +125,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: /** The set of capabilities that are hidden by a polymorphic result type * of some previous definition. */ - private var defsShadow: Refs = SimpleIdentitySet.empty + private var defsShadow: Refs = emptySet /** A map from definitions to their internal result types. * Populated during separation checking traversal. @@ -58,6 +137,16 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: */ private var previousDefs: List[mutable.ListBuffer[ValOrDefDef]] = Nil + private var consumed: MutConsumedSet = MutConsumedSet() + + private def withFreshConsumed(op: => Unit): Unit = + val saved = consumed + consumed = MutConsumedSet() + op + consumed = saved + + private var openLabeled: List[(Name, mutable.ListBuffer[ConsumedSet])] = Nil + extension (refs: Refs) private def footprint(using Context): Refs = def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match @@ -198,6 +287,19 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: tree.srcPos) end sepUseError + def consumeError(ref: CaptureRef, loc: SrcPos, pos: SrcPos)(using Context): Unit = + report.error( + em"""Separation failure: Illegal access to $ref, + |which was passed to a @consume parameter on line ${loc.line + 1} + |and therefore is no longer available.""", + pos) + + def consumeInLoopError(ref: CaptureRef, pos: SrcPos)(using Context): Unit = + report.error( + em"""Separation failure: $ref appears in a loop, + |therefore it cannot be passed to a @consume parameter.""", + pos) + private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = val fnCaptures = methPart(fn) match case Select(qual, _) => qual.nuType.captureSet @@ -240,6 +342,9 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val overlap = defUseOverlap(defsShadow, usedFootprint, tree.symbol) if !overlap.isEmpty then sepUseError(tree, usedFootprint, overlap) + for ref <- used.elems do + val pos = consumed.get(ref) + if pos != null then consumeError(ref, pos, tree.srcPos) def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = checkType(tpt.nuType, tpt.srcPos, @@ -383,10 +488,11 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: checkRefs(toCheck, i"$typeDescr type $tpe hides") case TypeKind.Argument(arg) => if tpe.hasAnnotation(defn.ConsumeAnnot) then - val capts = captures(arg) - def descr(verb: String) = i"argument to @consume parameter with type ${arg.nuType} $verb" - checkRefs(capts.footprint, descr("refers to")) - checkRefs(capts.hidden.footprint, descr("hides")) + val capts = captures(arg).footprint + checkRefs(capts, i"argument to @consume parameter with type ${arg.nuType} refers to") + for ref <- capts do + if !ref.derivesFrom(defn.Caps_SharedCapability) then + consumed.put(ref, arg.srcPos) if !tpe.hasAnnotation(defn.UntrackedCapturesAnnot) then traverse(Captures.None, tpe) @@ -435,15 +541,24 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case tree: Apply => tree.symbol == defn.Caps_unsafeAssumeSeparate case _ => false + def checkValOrDefDef(tree: ValOrDefDef)(using Context): Unit = + if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then + checkType(tree.tpt, tree.symbol) + if previousDefs.nonEmpty then + capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hidden.footprint}") + defsShadow ++= captures(tree.tpt).hidden.footprint.deductSym(tree.symbol) + resultType(tree.symbol) = tree.tpt.nuType + previousDefs.head += tree + def traverse(tree: Tree)(using Context): Unit = if isUnsafeAssumeSeparate(tree) then return checkUse(tree) tree match case tree: GenericApply => + traverseChildren(tree) tree.tpe match case _: MethodOrPoly => case _ => traverseApply(tree, Nil) - traverseChildren(tree) case tree: Block => val saved = defsShadow previousDefs = mutable.ListBuffer() :: previousDefs @@ -451,19 +566,47 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: finally previousDefs = previousDefs.tail defsShadow = saved - case tree: ValOrDefDef => + case tree: ValDef => traverseChildren(tree) - if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then - checkType(tree.tpt, tree.symbol) - if previousDefs.nonEmpty then - capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hidden.footprint}") - defsShadow ++= captures(tree.tpt).hidden.footprint.deductSym(tree.symbol) - resultType(tree.symbol) = tree.tpt.nuType - previousDefs.head += tree + checkValOrDefDef(tree) + case tree: DefDef => + withFreshConsumed: + traverseChildren(tree) + checkValOrDefDef(tree) + case If(cond, thenp, elsep) => + traverse(cond) + val thenConsumed = consumed.segment(traverse(thenp)) + val elseConsumed = consumed.segment(traverse(elsep)) + consumed ++= thenConsumed + consumed ++= elseConsumed + case tree @ Labeled(bind, expr) => + val consumedBuf = mutable.ListBuffer[ConsumedSet]() + openLabeled = (bind.name, consumedBuf) :: openLabeled + traverse(expr) + for cs <- consumedBuf do consumed ++= cs + openLabeled = openLabeled.tail + case Return(expr, from) => + val retConsumed = consumed.segment(traverse(expr)) + from match + case Ident(name) => + for (lbl, consumedBuf) <- openLabeled do + if lbl == name then + consumedBuf += retConsumed + case _ => + case Match(sel, cases) => + // Matches without returns might still be kept after pattern matching to + // encode table switches. + traverse(sel) + val caseConsumed = for cas <- cases yield consumed.segment(traverse(cas)) + caseConsumed.foreach(consumed ++= _) + case tree: TypeDef if tree.symbol.isClass => + withFreshConsumed: + traverseChildren(tree) + case tree: WhileDo => + val loopConsumed = consumed.segment(traverseChildren(tree)) + if loopConsumed.size != 0 then + val (ref, pos) = loopConsumed.toMap.head + consumeInLoopError(ref, pos) case _ => traverseChildren(tree) -end SepChecker - - - - +end SepChecker \ No newline at end of file diff --git a/tests/neg-custom-args/captures/linear-buffer.check b/tests/neg-custom-args/captures/linear-buffer.check new file mode 100644 index 000000000000..b6aa77dc2f84 --- /dev/null +++ b/tests/neg-custom-args/captures/linear-buffer.check @@ -0,0 +1,29 @@ +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:13:17 ----------------------------------------------------- +13 | val buf3 = app(buf, 3) // error + | ^^^ + | Separation failure: Illegal access to (buf : Buffer[Int]^), + | which was passed to a @consume parameter on line 11 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:20:17 ----------------------------------------------------- +20 | val buf3 = app(buf1, 4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), + | which was passed to a @consume parameter on line 18 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:28:17 ----------------------------------------------------- +28 | val buf3 = app(buf1, 4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), + | which was passed to a @consume parameter on line 25 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:38:17 ----------------------------------------------------- +38 | val buf3 = app(buf1, 4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), + | which was passed to a @consume parameter on line 33 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:42:8 ------------------------------------------------------ +42 | app(buf, 1) // error + | ^^^ + | Separation failure: (buf : Buffer[Int]^) appears in a loop, + | therefore it cannot be passed to a @consume parameter. diff --git a/tests/neg-custom-args/captures/linear-buffer.scala b/tests/neg-custom-args/captures/linear-buffer.scala new file mode 100644 index 000000000000..4cd7d9a3d85b --- /dev/null +++ b/tests/neg-custom-args/captures/linear-buffer.scala @@ -0,0 +1,42 @@ +import caps.{cap, consume, Mutable} +import language.experimental.captureChecking + +class Buffer[T] extends Mutable: + mut def append(x: T): Buffer[T]^ = ??? + +def app[T](@consume buf: Buffer[T]^, elem: T): Buffer[T]^ = + buf.append(elem) + +def Test(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = app(buf, 1) + val buf2 = app(buf1, 2) // OK + val buf3 = app(buf, 3) // error + +def Test2(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = app(buf, 1) + val buf2 = + if ??? then app(buf1, 2) // OK + else app(buf1, 3) // OK + val buf3 = app(buf1, 4) // error + +def Test3(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = app(buf, 1) + val buf2 = (??? : Int) match + case 1 => app(buf1, 2) // OK + case 2 => app(buf1, 2) + case _ => app(buf1, 3) + val buf3 = app(buf1, 4) // error + +def Test4(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = app(buf, 1) + val buf2 = (??? : Int) match + case 1 => app(buf1, 2) // OK + case 2 => app(buf1, 2) + case 3 => app(buf1, 3) + case 4 => app(buf1, 4) + case 5 => app(buf1, 5) + val buf3 = app(buf1, 4) // error + +def Test5(@consume buf: Buffer[Int]^) = + while true do + app(buf, 1) // error diff --git a/tests/neg-custom-args/captures/non-local-consume.scala b/tests/neg-custom-args/captures/non-local-consume.scala index cfdbe4a4aa0c..0e46146eb7e8 100644 --- a/tests/neg-custom-args/captures/non-local-consume.scala +++ b/tests/neg-custom-args/captures/non-local-consume.scala @@ -21,3 +21,9 @@ def f4(@consume buf: Buffer^): Buffer^ = def g(): Buffer^ = buf1 // error g() +def f5(@consume buf: Buffer^): Unit = + val buf1: Buffer^ = buf + def g(): Unit = cc(buf1) // error + g() + +def cc(@consume buf: Buffer^): Unit = () From 66fc880a78b196927a5ac86101cc4cab912f5ce5 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Jan 2025 22:38:05 +0100 Subject: [PATCH 18/93] Check accesses to non-local this in hidden sets Allow them only in @consume methods --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 3 +- .../src/dotty/tools/dotc/cc/SepCheck.scala | 33 ++++++++++---- compiler/src/dotty/tools/dotc/cc/Setup.scala | 25 +++++++---- .../neg-custom-args/captures/bad-uses-2.scala | 2 +- .../captures/linear-buffer.check | 43 +++++++++++++------ .../captures/linear-buffer.scala | 8 +++- 6 files changed, 80 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 2278ee5d1f2c..a03af783b8e8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -281,7 +281,8 @@ extension (tp: Type) /** The first element of this path type */ final def pathRoot(using Context): Type = tp.dealias match - case tp1: NamedType if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot + case tp1: TermRef if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot + case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot case tp1 => tp1 /** If this part starts with `C.this`, the class `C`. diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 47c394879255..a37f0b97d8d8 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -455,15 +455,30 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val badParams = mutable.ListBuffer[Symbol]() def currentOwner = kind.dclSym.orElse(ctx.owner) for hiddenRef <- prune(refsToCheck) do - val refSym = hiddenRef.pathRoot.termSymbol // TODO also hangle ThisTypes as pathRoots - if refSym.exists && !refSym.info.derivesFrom(defn.Caps_SharedCapability) then - if currentOwner.enclosingMethodOrClass.isProperlyContainedIn(refSym.owner.enclosingMethodOrClass) then - report.error(em"""Separation failure: $descr non-local $refSym""", pos) - else if refSym.is(TermParam) - && !refSym.hasAnnotation(defn.ConsumeAnnot) - && currentOwner.isContainedIn(refSym.owner) - then - badParams += refSym + val proot = hiddenRef.pathRoot + if !proot.widen.derivesFrom(defn.Caps_SharedCapability) then + proot match + case ref: TermRef => + val refSym = ref.symbol + if currentOwner.enclosingMethodOrClass.isProperlyContainedIn(refSym.maybeOwner.enclosingMethodOrClass) then + report.error(em"""Separation failure: $descr non-local $refSym""", pos) + else if refSym.is(TermParam) + && !refSym.hasAnnotation(defn.ConsumeAnnot) + && currentOwner.isContainedIn(refSym.owner) + then + badParams += refSym + case ref: ThisType => + val encl = currentOwner.enclosingMethodOrClass + if encl.isProperlyContainedIn(ref.cls) + && !encl.is(Synthetic) + && !encl.hasAnnotation(defn.ConsumeAnnot) + then + report.error( + em"""Separation failure: $descr non-local this of class ${ref.cls}. + |The access must be in a @consume method to allow this.""", + pos) + case _ => + if badParams.nonEmpty then def paramsStr(params: List[Symbol]): String = (params: @unchecked) match case p :: Nil => i"${p.name}" diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index aa85a315ecef..dd0407c11cfe 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -698,15 +698,24 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: */ def checkProperUseOrConsume(tree: Tree)(using Context): Unit = tree match case tree: MemberDef => - for ann <- tree.symbol.annotations do - def isAllowedFor(sym: Symbol) = - (sym.is(Param) || sym.is(ParamAccessor)) - && (ann.symbol != defn.ConsumeAnnot || sym.isTerm) + val sym = tree.symbol + def isMethodParam = (sym.is(Param) || sym.is(ParamAccessor)) && !sym.owner.isAnonymousFunction - def termStr = - if ann.symbol == defn.ConsumeAnnot then " term" else "" - if defn.ccParamOnlyAnnotations.contains(ann.symbol) && !isAllowedFor(tree.symbol) then - report.error(i"Only$termStr parameters of methods can have @${ann.symbol.name} annotations", tree.srcPos) + for ann <- tree.symbol.annotations do + val annotCls = ann.symbol + if annotCls == defn.ConsumeAnnot then + if !(isMethodParam && sym.isTerm) + && !(sym.is(Method) && sym.owner.isClass) + then + report.error( + em"""@consume cannot be used here. Only memeber methods and their term parameters + |can have @consume annotations.""", + tree.srcPos) + else if annotCls == defn.UseAnnot then + if !isMethodParam then + report.error( + em"@use cannot be used here. Only method parameters can have @use annotations.", + tree.srcPos) case _ => end checkProperUseOrConsume end setupTraverser diff --git a/tests/neg-custom-args/captures/bad-uses-2.scala b/tests/neg-custom-args/captures/bad-uses-2.scala index 7239c81b2f9a..2b4d6eebb2f0 100644 --- a/tests/neg-custom-args/captures/bad-uses-2.scala +++ b/tests/neg-custom-args/captures/bad-uses-2.scala @@ -6,7 +6,7 @@ class TestUse: def foo[@use T](@use c: T): Unit = ??? // OK class TestConsume: - @consume def F = ??? // error + @consume def F = ??? // ok @consume val x = ??? // error @consume type T // error def foo[@consume T](@use c: T): Unit = ??? // error diff --git a/tests/neg-custom-args/captures/linear-buffer.check b/tests/neg-custom-args/captures/linear-buffer.check index b6aa77dc2f84..a3a2c2c40fb4 100644 --- a/tests/neg-custom-args/captures/linear-buffer.check +++ b/tests/neg-custom-args/captures/linear-buffer.check @@ -1,29 +1,44 @@ --- Error: tests/neg-custom-args/captures/linear-buffer.scala:13:17 ----------------------------------------------------- -13 | val buf3 = app(buf, 3) // error +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:5:24 ------------------------------------------------------ +5 | mut def append(x: T): BadBuffer[T]^ = this // error + | ^^^^^^^^^^^^^ + | Separation failure: method append's result type BadBuffer[T]^ hides non-local this of class class BadBuffer. + | The access must be in a @consume method to allow this. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:7:13 ------------------------------------------------------ +7 | def bar: BadBuffer[T]^ = this // error + | ^^^^^^^^^^^^^ + | Separation failure: method bar's result type BadBuffer[T]^ hides non-local this of class class BadBuffer. + | The access must be in a @consume method to allow this. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:6:9 ------------------------------------------------------- +6 | def foo = // error + | ^ + |Separation failure: method foo's inferred result type BadBuffer[box T^?]^ hides non-local this of class class BadBuffer. + |The access must be in a @consume method to allow this. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:19:17 ----------------------------------------------------- +19 | val buf3 = app(buf, 3) // error | ^^^ | Separation failure: Illegal access to (buf : Buffer[Int]^), - | which was passed to a @consume parameter on line 11 + | which was passed to a @consume parameter on line 17 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/linear-buffer.scala:20:17 ----------------------------------------------------- -20 | val buf3 = app(buf1, 4) // error +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:26:17 ----------------------------------------------------- +26 | val buf3 = app(buf1, 4) // error | ^^^^ | Separation failure: Illegal access to (buf1 : Buffer[Int]^), - | which was passed to a @consume parameter on line 18 + | which was passed to a @consume parameter on line 24 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/linear-buffer.scala:28:17 ----------------------------------------------------- -28 | val buf3 = app(buf1, 4) // error +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:34:17 ----------------------------------------------------- +34 | val buf3 = app(buf1, 4) // error | ^^^^ | Separation failure: Illegal access to (buf1 : Buffer[Int]^), - | which was passed to a @consume parameter on line 25 + | which was passed to a @consume parameter on line 31 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/linear-buffer.scala:38:17 ----------------------------------------------------- -38 | val buf3 = app(buf1, 4) // error +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:44:17 ----------------------------------------------------- +44 | val buf3 = app(buf1, 4) // error | ^^^^ | Separation failure: Illegal access to (buf1 : Buffer[Int]^), - | which was passed to a @consume parameter on line 33 + | which was passed to a @consume parameter on line 39 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/linear-buffer.scala:42:8 ------------------------------------------------------ -42 | app(buf, 1) // error +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:48:8 ------------------------------------------------------ +48 | app(buf, 1) // error | ^^^ | Separation failure: (buf : Buffer[Int]^) appears in a loop, | therefore it cannot be passed to a @consume parameter. diff --git a/tests/neg-custom-args/captures/linear-buffer.scala b/tests/neg-custom-args/captures/linear-buffer.scala index 4cd7d9a3d85b..97315c1aa0fb 100644 --- a/tests/neg-custom-args/captures/linear-buffer.scala +++ b/tests/neg-custom-args/captures/linear-buffer.scala @@ -1,8 +1,14 @@ import caps.{cap, consume, Mutable} import language.experimental.captureChecking +class BadBuffer[T] extends Mutable: + mut def append(x: T): BadBuffer[T]^ = this // error + def foo = // error + def bar: BadBuffer[T]^ = this // error + bar + class Buffer[T] extends Mutable: - mut def append(x: T): Buffer[T]^ = ??? + @consume mut def append(x: T): Buffer[T]^ = this // ok def app[T](@consume buf: Buffer[T]^, elem: T): Buffer[T]^ = buf.append(elem) From 821e17aa41f06c362d2c38176c9ade8acb80a8f8 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 29 Jan 2025 23:25:15 +0100 Subject: [PATCH 19/93] Check that @consumed prefix capabilities are not re-used Also: Fixes to computations of overlapWith and -- on Refs that take account of pathss, where shorter paths cover deeper ones. --- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 26 ++ .../src/dotty/tools/dotc/cc/SepCheck.scala | 222 +++++++++++------- .../src/scala/collection/View.scala | 4 +- .../mutable/CheckedIndexedSeqView.scala | 28 +-- .../captures/linear-buffer-2.check | 29 +++ .../captures/linear-buffer-2.scala | 42 ++++ .../captures/linear-buffer.check | 20 +- .../captures/path-patmat-should-be-pos.scala | 4 +- .../captures/filter-iterable.scala | 11 + .../colltest5/CollectionStrawManCC5_1.scala | 8 +- 10 files changed, 280 insertions(+), 114 deletions(-) create mode 100644 tests/neg-custom-args/captures/linear-buffer-2.check create mode 100644 tests/neg-custom-args/captures/linear-buffer-2.scala create mode 100644 tests/pos-custom-args/captures/filter-iterable.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index d969aa7f90db..f95722274258 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -259,6 +259,32 @@ trait CaptureRef extends TypeProxy, ValueType: case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) case _ => false + /** `x covers y` if we should retain `y` when computing the overlap of + * two footprints which have `x` respectively `y` as elements. + * We assume that .rd have already been stripped on both sides. + * We have: + * + * x covers x + * x covers y ==> x covers y.f + * x covers y ==> x* covers y*, x? covers y? + * TODO what other clauses from subsumes do we need to port here? + */ + final def covers(y: CaptureRef)(using Context): Boolean = + (this eq y) + || y.match + case y @ TermRef(ypre: CaptureRef, _) if !y.isCap => + this.covers(ypre) + case ReachCapability(y1) => + this match + case ReachCapability(x1) => x1.covers(y1) + case _ => false + case MaybeCapability(y1) => + this match + case MaybeCapability(x1) => x1.covers(y1) + case _ => false + case _ => + false + def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index a37f0b97d8d8..c2236e702fe4 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -30,15 +30,28 @@ object SepChecker: else NeedsCheck end Captures - /** The kind of checked type, used for composing error messages */ - enum TypeKind: + /** The role in which a checked type appears, used for composing error messages */ + enum TypeRole: case Result(sym: Symbol, inferred: Boolean) case Argument(arg: Tree) + case Qualifier(qual: Tree, meth: Symbol) + /** If this is a Result tole, the associated symbol, otherwise NoSymbol */ def dclSym = this match case Result(sym, _) => sym case _ => NoSymbol - end TypeKind + + /** A textual description of this role */ + def description(using Context): String = this match + case Result(sym, inferred) => + def inferredStr = if inferred then " inferred" else "" + def resultStr = if sym.info.isInstanceOf[MethodicType] then " result" else "" + i"$sym's$inferredStr$resultStr type" + case TypeRole.Argument(_) => + "the argument's adapted type" + case TypeRole.Qualifier(_, meth) => + i"the type of the qualifier to a call of $meth" + end TypeRole /** A class for segmented sets of consumed references. * References are associated with the source positions where they first appeared. @@ -161,10 +174,29 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: private def overlapWith(other: Refs)(using Context): Refs = val refs1 = refs val refs2 = other + + /** Exclusive capabilities in refs1 that are covered by exclusive or + * stripped read-only capabilties in refs2 + * + stripped read-only capabilities in refs1 that are covered by an + * exclusive capability in refs2. + */ def common(refs1: Refs, refs2: Refs) = refs1.filter: ref => - ref.isExclusive && refs2.exists(_.stripReadOnly eq ref) + ref.isExclusive && refs2.exists(ref2 => ref2.stripReadOnly.covers(ref)) + ++ + refs1 + .filter: + case ReadOnlyCapability(ref @ TermRef(prefix: CaptureRef, _)) => + // We can get away testing only references with at least one field selection + // here since stripped readOnly references that equal a reference in refs2 + // are added by the first clause of the symmetric call to common. + !ref.isCap && refs2.exists(ref2 => ref2.covers(prefix)) + case _ => + false + .map(_.stripReadOnly) + common(refs, other) ++ common(other, refs) + end overlapWith private def hidden(using Context): Refs = val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet @@ -179,16 +211,20 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: refs.exists: ref => !hiddenByElem(ref, _ => emptySet).isEmpty + private def deduct(others: Refs)(using Context): Refs = + refs.filter: ref => + !others.exists(_.covers(ref)) + /** Deduct the footprint of `sym` and `sym*` from `refs` */ - private def deductSym(sym: Symbol)(using Context) = + private def deductSym(sym: Symbol)(using Context): Refs = val ref = sym.termRef - if ref.isTrackableRef then refs -- CaptureSet(ref, ref.reach).elems.footprint + if ref.isTrackableRef then refs.deduct(CaptureSet(ref, ref.reach).elems.footprint) else refs /** Deduct the footprint of all captures of `deps` from `refs` */ private def deductCapturesOf(deps: List[Tree])(using Context): Refs = deps.foldLeft(refs): (refs, dep) => - refs -- captures(dep).footprint + refs.deduct(captures(dep).footprint) end extension private def hiddenByElem(ref: CaptureRef, recur: Refs => Refs)(using Context): Refs = ref match @@ -249,7 +285,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def clashFootprint = clashCaptures.footprint def hiddenFootprint = hiddenCaptures.footprint def declaredFootprint = deps(arg).map(captures(_)).foldLeft(emptySet)(_ ++ _).footprint - def footprintOverlap = hiddenFootprint.overlapWith(clashFootprint) -- declaredFootprint + def footprintOverlap = hiddenFootprint.overlapWith(clashFootprint).deduct(declaredFootprint) report.error( em"""Separation failure: argument of type ${arg.nuType} |to $funStr @@ -289,15 +325,15 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def consumeError(ref: CaptureRef, loc: SrcPos, pos: SrcPos)(using Context): Unit = report.error( - em"""Separation failure: Illegal access to $ref, - |which was passed to a @consume parameter on line ${loc.line + 1} + em"""Separation failure: Illegal access to $ref, which was passed to a + |@consume parameter or was used as a prefix to a @consume method on line ${loc.line + 1} |and therefore is no longer available.""", pos) def consumeInLoopError(ref: CaptureRef, pos: SrcPos)(using Context): Unit = report.error( - em"""Separation failure: $ref appears in a loop, - |therefore it cannot be passed to a @consume parameter.""", + em"""Separation failure: $ref appears in a loop, therefore it cannot + |be passed to a @consume parameter or be used as a prefix of a @consume method call.""", pos) private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = @@ -316,7 +352,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: for (arg, idx) <- indexedArgs do if arg.needsSepCheck then val ac = formalCaptures(arg) - checkType(arg.formalType, arg.srcPos, TypeKind.Argument(arg)) + checkType(arg.formalType, arg.srcPos, TypeRole.Argument(arg)) val hiddenInArg = ac.hidden.footprint //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") val overlap = hiddenInArg.overlapWith(footprint).deductCapturesOf(deps(arg)) @@ -331,7 +367,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: resultType.get(sym) match case Some(tp) if !overlap.isEmpty => val declared = tp.captureSet.elems - overlap -- declared.footprint -- declared.hidden.footprint + overlap.deduct(declared.footprint).deduct(declared.hidden.footprint) case _ => overlap @@ -346,30 +382,79 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val pos = consumed.get(ref) if pos != null then consumeError(ref, pos, tree.srcPos) - def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = - checkType(tpt.nuType, tpt.srcPos, - TypeKind.Result(sym, inferred = tpt.isInstanceOf[InferredTypeTree])) + def explicitRefs(tp: Type): Refs = tp match + case tp: (TermRef | ThisType) => SimpleIdentitySet(tp) + case AnnotatedType(parent, _) => explicitRefs(parent) + case AndType(tp1, tp2) => explicitRefs(tp1) ++ explicitRefs(tp2) + case OrType(tp1, tp2) => explicitRefs(tp1) ** explicitRefs(tp2) + case _ => emptySet - /** Check that all parts of type `tpe` are separated. */ - def checkType(tpe: Type, pos: SrcPos, kind: TypeKind)(using Context): Unit = + def prune(refs: Refs, tpe: Type, role: TypeRole)(using Context): Refs = + refs.deductSym(role.dclSym).deduct(explicitRefs(tpe)) - def typeDescr = kind match - case TypeKind.Result(sym, inferred) => - def inferredStr = if inferred then " inferred" else "" - def resultStr = if sym.info.isInstanceOf[MethodicType] then " result" else "" - i"$sym's$inferredStr$resultStr" - case TypeKind.Argument(_) => - "the argument's adapted" + def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = + checkType(tpt.nuType, tpt.srcPos, + TypeRole.Result(sym, inferred = tpt.isInstanceOf[InferredTypeTree])) + + /** Check validity consumed references `refsToCheck`. The references are consumed + * because they are hidden in a Fresh.Cap result type or they are referred + * to in an argument to a @consume parameter or in a prefix of a @consume method -- + * which one applie is determined by the role parameter. + * @param refsToCheck the referencves to check + * @param tpe the type containing those references + * @param role the role in which the type apears + * @param descr a textual description of the type and its relationship with the checked reference + * @param pos position for error reporting + */ + def checkConsumedRefs(refsToCheck: Refs, tpe: Type, role: TypeRole, descr: => String, pos: SrcPos)(using Context) = + val badParams = mutable.ListBuffer[Symbol]() + def currentOwner = role.dclSym.orElse(ctx.owner) + for hiddenRef <- prune(refsToCheck, tpe, role) do + val proot = hiddenRef.pathRoot + if !proot.widen.derivesFrom(defn.Caps_SharedCapability) then + proot match + case ref: TermRef => + val refSym = ref.symbol + if currentOwner.enclosingMethodOrClass.isProperlyContainedIn(refSym.maybeOwner.enclosingMethodOrClass) then + report.error(em"""Separation failure: $descr non-local $refSym""", pos) + else if refSym.is(TermParam) + && !refSym.hasAnnotation(defn.ConsumeAnnot) + && currentOwner.isContainedIn(refSym.owner) + then + badParams += refSym + case ref: ThisType => + val encl = currentOwner.enclosingMethodOrClass + if encl.isProperlyContainedIn(ref.cls) + && !encl.is(Synthetic) + && !encl.hasAnnotation(defn.ConsumeAnnot) + then + report.error( + em"""Separation failure: $descr non-local this of class ${ref.cls}. + |The access must be in a @consume method to allow this.""", + pos) + case _ => - def explicitRefs(tp: Type): Refs = tp match - case tp: (TermRef | ThisType) => SimpleIdentitySet(tp) - case AnnotatedType(parent, _) => explicitRefs(parent) - case AndType(tp1, tp2) => explicitRefs(tp1) ++ explicitRefs(tp2) - case OrType(tp1, tp2) => explicitRefs(tp1) ** explicitRefs(tp2) - case _ => emptySet + if badParams.nonEmpty then + def paramsStr(params: List[Symbol]): String = (params: @unchecked) match + case p :: Nil => i"${p.name}" + case p :: p2 :: Nil => i"${p.name} and ${p2.name}" + case p :: ps => i"${p.name}, ${paramsStr(ps)}" + val (pluralS, singleS) = if badParams.tail.isEmpty then ("", "s") else ("s", "") + report.error( + em"""Separation failure: $descr parameter$pluralS ${paramsStr(badParams.toList)}. + |The parameter$pluralS need$singleS to be annotated with @consume to allow this.""", + pos) + + role match + case _: TypeRole.Argument | _: TypeRole.Qualifier => + for ref <- refsToCheck do + if !ref.derivesFrom(defn.Caps_SharedCapability) then + consumed.put(ref, pos) + case _ => + end checkConsumedRefs - def prune(refs: Refs): Refs = - refs.deductSym(kind.dclSym) -- explicitRefs(tpe) + /** Check that all parts of type `tpe` are separated. */ + def checkType(tpe: Type, pos: SrcPos, role: TypeRole)(using Context): Unit = def checkParts(parts: List[Type]): Unit = var footprint: Refs = emptySet @@ -391,21 +476,21 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if !globalOverlap.isEmpty then val (prevStr, prevRefs, overlap) = parts.iterator.take(checked) .map: prev => - val prevRefs = prune(mapRefs(prev.deepCaptureSet.elems).footprint) + val prevRefs = prune(mapRefs(prev.deepCaptureSet.elems).footprint, tpe, role) (i", $prev , ", prevRefs, prevRefs.overlapWith(next)) .dropWhile(_._3.isEmpty) .nextOption .getOrElse(("", current, globalOverlap)) report.error( - em"""Separation failure in $typeDescr type $tpe. + em"""Separation failure in ${role.description} $tpe. |One part, $part , $nextRel ${CaptureSet(next)}. |A previous part$prevStr $prevRel ${CaptureSet(prevRefs)}. |The two sets overlap at ${CaptureSet(overlap)}.""", pos) val partRefs = part.deepCaptureSet.elems - val partFootprint = prune(partRefs.footprint) - val partHidden = prune(partRefs.hidden.footprint) -- partFootprint + val partFootprint = prune(partRefs.footprint, tpe, role) + val partHidden = prune(partRefs.hidden.footprint, tpe, role).deduct(partFootprint) checkSep(footprint, partHidden, identity, "references", "hides") checkSep(hiddenSet, partHidden, _.hidden, "also hides", "hides") @@ -451,47 +536,8 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case t => foldOver(c, t) - def checkRefs(refsToCheck: Refs, descr: => String) = - val badParams = mutable.ListBuffer[Symbol]() - def currentOwner = kind.dclSym.orElse(ctx.owner) - for hiddenRef <- prune(refsToCheck) do - val proot = hiddenRef.pathRoot - if !proot.widen.derivesFrom(defn.Caps_SharedCapability) then - proot match - case ref: TermRef => - val refSym = ref.symbol - if currentOwner.enclosingMethodOrClass.isProperlyContainedIn(refSym.maybeOwner.enclosingMethodOrClass) then - report.error(em"""Separation failure: $descr non-local $refSym""", pos) - else if refSym.is(TermParam) - && !refSym.hasAnnotation(defn.ConsumeAnnot) - && currentOwner.isContainedIn(refSym.owner) - then - badParams += refSym - case ref: ThisType => - val encl = currentOwner.enclosingMethodOrClass - if encl.isProperlyContainedIn(ref.cls) - && !encl.is(Synthetic) - && !encl.hasAnnotation(defn.ConsumeAnnot) - then - report.error( - em"""Separation failure: $descr non-local this of class ${ref.cls}. - |The access must be in a @consume method to allow this.""", - pos) - case _ => - - if badParams.nonEmpty then - def paramsStr(params: List[Symbol]): String = (params: @unchecked) match - case p :: Nil => i"${p.name}" - case p :: p2 :: Nil => i"${p.name} and ${p2.name}" - case p :: ps => i"${p.name}, ${paramsStr(ps)}" - val (pluralS, singleS) = if badParams.tail.isEmpty then ("", "s") else ("s", "") - report.error( - em"""Separation failure: $descr parameter$pluralS ${paramsStr(badParams.toList)}. - |The parameter$pluralS need$singleS to be annotated with @consume to allow this.""", - pos) - - def checkLegalRefs() = kind match - case TypeKind.Result(sym, _) => + def checkLegalRefs() = role match + case TypeRole.Result(sym, _) => if !sym.isAnonymousFunction // we don't check return types of anonymous functions && !sym.is(Case) // We don't check so far binders in patterns since they // have inferred universal types. TODO come back to this; @@ -499,15 +545,13 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: // "see through them" when we look at hidden sets. then val refs = tpe.deepCaptureSet.elems - val toCheck = refs.hidden.footprint -- refs.footprint - checkRefs(toCheck, i"$typeDescr type $tpe hides") - case TypeKind.Argument(arg) => + val toCheck = refs.hidden.footprint.deduct(refs.footprint) + checkConsumedRefs(toCheck, tpe, role, i"${role.description} $tpe hides", pos) + case TypeRole.Argument(arg) => if tpe.hasAnnotation(defn.ConsumeAnnot) then val capts = captures(arg).footprint - checkRefs(capts, i"argument to @consume parameter with type ${arg.nuType} refers to") - for ref <- capts do - if !ref.derivesFrom(defn.Caps_SharedCapability) then - consumed.put(ref, arg.srcPos) + checkConsumedRefs(capts, tpe, role, i"argument to @consume parameter with type ${arg.nuType} refers to", pos) + case _ => if !tpe.hasAnnotation(defn.UntrackedCapturesAnnot) then traverse(Captures.None, tpe) @@ -569,6 +613,12 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if isUnsafeAssumeSeparate(tree) then return checkUse(tree) tree match + case tree @ Select(qual, _) if tree.symbol.is(Method) && tree.symbol.hasAnnotation(defn.ConsumeAnnot) => + traverseChildren(tree) + checkConsumedRefs( + captures(qual).footprint, qual.nuType, + TypeRole.Qualifier(qual, tree.symbol), + i"call prefix of @consume ${tree.symbol} refers to", qual.srcPos) case tree: GenericApply => traverseChildren(tree) tree.tpe match diff --git a/scala2-library-cc/src/scala/collection/View.scala b/scala2-library-cc/src/scala/collection/View.scala index c5d582eb2a11..b30fa5e508fe 100644 --- a/scala2-library-cc/src/scala/collection/View.scala +++ b/scala2-library-cc/src/scala/collection/View.scala @@ -16,6 +16,7 @@ import scala.annotation.{nowarn, tailrec} import scala.collection.mutable.{ArrayBuffer, Builder} import scala.collection.immutable.LazyList import language.experimental.captureChecking +import caps.unsafe.unsafeAssumeSeparate /** Views are collections whose transformation operations are non strict: the resulting elements * are evaluated only when the view is effectively traversed (e.g. using `foreach` or `foldLeft`), @@ -151,7 +152,8 @@ object View extends IterableFactory[View] { def apply[A](underlying: Iterable[A]^, p: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, p} = underlying match { case filter: Filter[A]^{underlying} if filter.isFlipped == isFlipped => - new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) + unsafeAssumeSeparate: + new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) case _ => new Filter(underlying, p, isFlipped) } } diff --git a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala index 9ce0399e0662..7bfda7972762 100644 --- a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala +++ b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala @@ -17,7 +17,7 @@ import language.experimental.captureChecking private[mutable] trait CheckedIndexedSeqView[+A] extends IndexedSeqView[A] { - protected val mutationCount: () => Int + protected val mutationCount: () -> Int override def iterator: Iterator[A]^{this} = new CheckedIndexedSeqView.CheckedIterator(this, mutationCount()) override def reverseIterator: Iterator[A]^{this} = new CheckedIndexedSeqView.CheckedReverseIterator(this, mutationCount()) @@ -42,7 +42,7 @@ private[mutable] object CheckedIndexedSeqView { import IndexedSeqView.SomeIndexedSeqOps @SerialVersionUID(3L) - private[mutable] class CheckedIterator[A](self: IndexedSeqView[A]^, mutationCount: => Int) + private[mutable] class CheckedIterator[A](self: IndexedSeqView[A]^, mutationCount: -> Int) extends IndexedSeqView.IndexedSeqViewIterator[A](self) { private[this] val expectedCount = mutationCount override def hasNext: Boolean = { @@ -52,7 +52,7 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A]^, mutationCount: => Int) + private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A]^, mutationCount: -> Int) extends IndexedSeqView.IndexedSeqViewReverseIterator[A](self) { private[this] val expectedCount = mutationCount override def hasNext: Boolean = { @@ -62,43 +62,43 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - class Id[+A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) + class Id[+A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int) extends IndexedSeqView.Id(underlying) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Appended[+A](underlying: SomeIndexedSeqOps[A]^, elem: A)(protected val mutationCount: () => Int) + class Appended[+A](underlying: SomeIndexedSeqOps[A]^, elem: A)(protected val mutationCount: () -> Int) extends IndexedSeqView.Appended(underlying, elem) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) + class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int) extends IndexedSeqView.Prepended(elem, underlying) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) + class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int) extends IndexedSeqView.Concat[A](prefix, suffix) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) + class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int) extends IndexedSeqView.Take(underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class TakeRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) + class TakeRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int) extends IndexedSeqView.TakeRight(underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Drop[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) + class Drop[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int) extends IndexedSeqView.Drop[A](underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class DropRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) + class DropRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int) extends IndexedSeqView.DropRight[A](underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Map[A, B](underlying: SomeIndexedSeqOps[A]^, f: A => B)(protected val mutationCount: () => Int) + class Map[A, B](underlying: SomeIndexedSeqOps[A]^, f: A => B)(protected val mutationCount: () -> Int) extends IndexedSeqView.Map(underlying, f) with CheckedIndexedSeqView[B] @SerialVersionUID(3L) - class Reverse[A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) + class Reverse[A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int) extends IndexedSeqView.Reverse[A](underlying) with CheckedIndexedSeqView[A] { override def reverse: IndexedSeqView[A] = underlying match { case x: IndexedSeqView[A] => x @@ -107,7 +107,7 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - class Slice[A](underlying: SomeIndexedSeqOps[A]^, from: Int, until: Int)(protected val mutationCount: () => Int) + class Slice[A](underlying: SomeIndexedSeqOps[A]^, from: Int, until: Int)(protected val mutationCount: () -> Int) extends AbstractIndexedSeqView[A] with CheckedIndexedSeqView[A] { protected val lo = from max 0 protected val hi = (until max 0) min underlying.length diff --git a/tests/neg-custom-args/captures/linear-buffer-2.check b/tests/neg-custom-args/captures/linear-buffer-2.check new file mode 100644 index 000000000000..3d64c432d116 --- /dev/null +++ b/tests/neg-custom-args/captures/linear-buffer-2.check @@ -0,0 +1,29 @@ +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:13:13 --------------------------------------------------- +13 | val buf3 = buf.append(3) // error + | ^^^ + | Separation failure: Illegal access to {buf} which is hidden by the previous definition + | of value buf1 with type Buffer[Int]^. + | This type hides capabilities {buf} +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:20:13 --------------------------------------------------- +20 | val buf3 = buf1.append(4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 18 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:28:13 --------------------------------------------------- +28 | val buf3 = buf1.append(4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 25 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:38:13 --------------------------------------------------- +38 | val buf3 = buf1.append(4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 33 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:42:4 ---------------------------------------------------- +42 | buf.append(1) // error + | ^^^ + | Separation failure: (buf : Buffer[Int]^) appears in a loop, therefore it cannot + | be passed to a @consume parameter or be used as a prefix of a @consume method call. diff --git a/tests/neg-custom-args/captures/linear-buffer-2.scala b/tests/neg-custom-args/captures/linear-buffer-2.scala new file mode 100644 index 000000000000..428171c3fab8 --- /dev/null +++ b/tests/neg-custom-args/captures/linear-buffer-2.scala @@ -0,0 +1,42 @@ +import caps.{cap, consume, Mutable} +import language.experimental.captureChecking + +class Buffer[T] extends Mutable: + @consume mut def append(x: T): Buffer[T]^ = this // ok + +def app[T](@consume buf: Buffer[T]^, elem: T): Buffer[T]^ = + buf.append(elem) + +def Test(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = buf.append(1) + val buf2 = buf1.append(2) // OK + val buf3 = buf.append(3) // error + +def Test2(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = buf.append(1) + val buf2 = + if ??? then buf1.append(2) // OK + else buf1.append(3) // OK + val buf3 = buf1.append(4) // error + +def Test3(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = buf.append(1) + val buf2 = (??? : Int) match + case 1 => buf1.append(2) // OK + case 2 => buf1.append(2) + case _ => buf1.append(3) + val buf3 = buf1.append(4) // error + +def Test4(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = buf.append(1) + val buf2 = (??? : Int) match + case 1 => buf1.append(2) // OK + case 2 => buf1.append(2) + case 3 => buf1.append(3) + case 4 => buf1.append(4) + case 5 => buf1.append(5) + val buf3 = buf1.append(4) // error + +def Test5(@consume buf: Buffer[Int]^) = + while true do + buf.append(1) // error diff --git a/tests/neg-custom-args/captures/linear-buffer.check b/tests/neg-custom-args/captures/linear-buffer.check index a3a2c2c40fb4..16ba3bd096a2 100644 --- a/tests/neg-custom-args/captures/linear-buffer.check +++ b/tests/neg-custom-args/captures/linear-buffer.check @@ -16,29 +16,29 @@ -- Error: tests/neg-custom-args/captures/linear-buffer.scala:19:17 ----------------------------------------------------- 19 | val buf3 = app(buf, 3) // error | ^^^ - | Separation failure: Illegal access to (buf : Buffer[Int]^), - | which was passed to a @consume parameter on line 17 + | Separation failure: Illegal access to (buf : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 17 | and therefore is no longer available. -- Error: tests/neg-custom-args/captures/linear-buffer.scala:26:17 ----------------------------------------------------- 26 | val buf3 = app(buf1, 4) // error | ^^^^ - | Separation failure: Illegal access to (buf1 : Buffer[Int]^), - | which was passed to a @consume parameter on line 24 + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 24 | and therefore is no longer available. -- Error: tests/neg-custom-args/captures/linear-buffer.scala:34:17 ----------------------------------------------------- 34 | val buf3 = app(buf1, 4) // error | ^^^^ - | Separation failure: Illegal access to (buf1 : Buffer[Int]^), - | which was passed to a @consume parameter on line 31 + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 31 | and therefore is no longer available. -- Error: tests/neg-custom-args/captures/linear-buffer.scala:44:17 ----------------------------------------------------- 44 | val buf3 = app(buf1, 4) // error | ^^^^ - | Separation failure: Illegal access to (buf1 : Buffer[Int]^), - | which was passed to a @consume parameter on line 39 + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 39 | and therefore is no longer available. -- Error: tests/neg-custom-args/captures/linear-buffer.scala:48:8 ------------------------------------------------------ 48 | app(buf, 1) // error | ^^^ - | Separation failure: (buf : Buffer[Int]^) appears in a loop, - | therefore it cannot be passed to a @consume parameter. + | Separation failure: (buf : Buffer[Int]^) appears in a loop, therefore it cannot + | be passed to a @consume parameter or be used as a prefix of a @consume method call. diff --git a/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala b/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala index aca6102204a3..5f434a21cc8b 100644 --- a/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala +++ b/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala @@ -1,6 +1,8 @@ +import caps.cap + class It[A] -class Filter[A](val underlying: It[A]^, val p: A => Boolean) extends It[A] +class Filter[A](val underlying: It[A]^, val p: A ->{cap, underlying} Boolean) extends It[A] object Filter: def apply[A](underlying: It[A]^, p: A => Boolean): Filter[A]^{underlying, p} = underlying match diff --git a/tests/pos-custom-args/captures/filter-iterable.scala b/tests/pos-custom-args/captures/filter-iterable.scala new file mode 100644 index 000000000000..c8e80af4cd73 --- /dev/null +++ b/tests/pos-custom-args/captures/filter-iterable.scala @@ -0,0 +1,11 @@ +import caps.cap + +class It[A] + +class Filter[A](val underlying: It[A]^, val p: A ->{cap, underlying} Boolean) extends It[A] +object Filter: + def apply[A](underlying: It[A]^, p: A => Boolean): Filter[A]^{cap, p, underlying} = + underlying match + case filter: Filter[A]^ => + val x = new Filter(filter.underlying, a => filter.p(a) && p(a)) + x: Filter[A]^{filter, p} \ No newline at end of file diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index 4281f7a21128..e12890a9be9b 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -7,7 +7,7 @@ import annotation.unchecked.{uncheckedVariance, uncheckedCaptures} import annotation.tailrec import caps.cap import caps.untrackedCaptures -import language.`3.7` // sepchecks on +import caps.unsafe.unsafeAssumeSeparate /** A strawman architecture for new collections. It contains some * example collection classes and methods with the intent to expose @@ -460,7 +460,11 @@ object CollectionStrawMan5 { def apply[A](underlying: Iterable[A]^, pp: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, pp} = underlying match case filter: Filter[A]^{underlying} => - new Filter(filter.underlying, a => filter.p(a) && pp(a)) + unsafeAssumeSeparate: + // See filter-iterable.scala for a test where a variant of Filter + // works without the unsafeAssumeSeparate. But it requires significant + // changes compared to the version here. + new Filter(filter.underlying, a => filter.p(a) && pp(a)) case _ => new Filter(underlying, pp) case class Partition[A](val underlying: Iterable[A]^, p: A => Boolean) { From 3ed82a34ff8eb023544443649e359f1b0f8c26c0 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 30 Jan 2025 15:57:45 +0100 Subject: [PATCH 20/93] Allow SharableCapablity anywhere on a path --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 10 ++++++++++ compiler/src/dotty/tools/dotc/cc/SepCheck.scala | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a03af783b8e8..4c3747a47779 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -285,6 +285,16 @@ extension (tp: Type) case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot case tp1 => tp1 + /** The first element of a path type, but stop at references extending + * SharableCapability + */ + final def pathRootOrShared(using Context): Type = + if tp.derivesFrom(defn.Caps_SharedCapability) then tp + else tp.dealias match + case tp1: TermRef if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot + case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot + case tp1 => tp1 + /** If this part starts with `C.this`, the class `C`. * Otherwise, if it starts with a reference `r`, `r`'s owner. * Otherwise NoSymbol. diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index c2236e702fe4..1aee9c467450 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -410,7 +410,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val badParams = mutable.ListBuffer[Symbol]() def currentOwner = role.dclSym.orElse(ctx.owner) for hiddenRef <- prune(refsToCheck, tpe, role) do - val proot = hiddenRef.pathRoot + val proot = hiddenRef.pathRootOrShared if !proot.widen.derivesFrom(defn.Caps_SharedCapability) then proot match case ref: TermRef => @@ -448,7 +448,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: role match case _: TypeRole.Argument | _: TypeRole.Qualifier => for ref <- refsToCheck do - if !ref.derivesFrom(defn.Caps_SharedCapability) then + if !ref.pathRootOrShared.derivesFrom(defn.Caps_SharedCapability) then consumed.put(ref, pos) case _ => end checkConsumedRefs From 2d379383a2b45b46f690a56c419d4290561fb377 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 31 Jan 2025 14:17:12 +0100 Subject: [PATCH 21/93] Polishings --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 22 +++++-- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 14 +++-- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 60 ++++++++++++------- .../src/dotty/tools/dotc/cc/Existential.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 15 ++++- .../src/dotty/tools/dotc/cc/SepCheck.scala | 4 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 10 +++- .../dotty/tools/dotc/core/Definitions.scala | 2 - .../src/dotty/tools/dotc/typer/Checking.scala | 2 +- library/src/scala/caps.scala | 13 +++- project/Build.scala | 2 +- .../src/scala/collection/View.scala | 3 + .../immutable/LazyListIterable.scala | 15 +++-- .../mutable/CheckedIndexedSeqView.scala | 29 ++++----- .../captures/mut-outside-mutable.check | 4 +- .../colltest5/CollectionStrawManCC5_1.scala | 8 +-- .../captures/colltest5/Test_2.scala | 1 - 18 files changed, 133 insertions(+), 77 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 4c3747a47779..25516da531fe 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -232,7 +232,9 @@ extension (tp: Type) case tp @ ReachCapability(_) => tp.singletonCaptureSet case ReadOnlyCapability(ref) => - ref.deepCaptureSet(includeTypevars) + val refDcs = ref.deepCaptureSet(includeTypevars) + if refDcs.isConst then CaptureSet(refDcs.elems.map(_.readOnly)) + else refDcs // this case should not happen for correct programs case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet case _ => @@ -279,17 +281,19 @@ extension (tp: Type) case _ => tp - /** The first element of this path type */ + /** The first element of this path type. Note that class parameter references + * are of the form this.C but their pathroot is still this.C, not this. + */ final def pathRoot(using Context): Type = tp.dealias match case tp1: TermRef if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot case tp1 => tp1 /** The first element of a path type, but stop at references extending - * SharableCapability + * SharedCapability. */ final def pathRootOrShared(using Context): Type = - if tp.derivesFrom(defn.Caps_SharedCapability) then tp + if tp.derivesFromSharedCapability then tp else tp.dealias match case tp1: TermRef if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot @@ -427,6 +431,7 @@ extension (tp: Type) def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability) def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable) + def derivesFromSharedCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_SharedCapability) /** Drop @retains annotations everywhere */ def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling @@ -466,6 +471,11 @@ extension (tp: Type) * 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. + * + * Reach capabilities cannot wrap read-only capabilities or maybe capabilities. + * We have + * (x.rd).reach = x*.rd + * (x.rd)? = (x*)? */ def reach(using Context): CaptureRef = tp match case tp @ AnnotatedType(tp1: CaptureRef, annot) @@ -483,6 +493,10 @@ extension (tp: Type) /** 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*`. + * + * Read-only capabilities cannot wrap maybe capabilities + * but they can wrap reach capabilities. We have + * (x?).readOnly = (x.rd)? */ def readOnly(using Context): CaptureRef = tp match case tp @ AnnotatedType(tp1: CaptureRef, annot) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index f95722274258..9987b0e91121 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -100,12 +100,14 @@ trait CaptureRef extends TypeProxy, ValueType: /** 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` ? */ + /** Is this reference one of the generic root capabilities `cap` or `cap.rd` ? */ final def isRootCapability(using Context): Boolean = this match case ReadOnlyCapability(tp1) => tp1.isCapOrFresh case _ => isCapOrFresh - /** Is this reference capability that does not derive from another capability ? */ + /** Is this reference a capability that does not derive from another capability? + * Includes read-only versions of maximal capabilities. + */ 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) @@ -113,6 +115,10 @@ trait CaptureRef extends TypeProxy, ValueType: case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false + /** An exclusive capability is a capability that derives + * indirectly from a maximal capability without goinh through + * a read-only capability first. + */ final def isExclusive(using Context): Boolean = !isReadOnly && (isMaxCapability || captureSetOfInfo.isExclusive) @@ -159,8 +165,6 @@ trait CaptureRef extends TypeProxy, ValueType: * X: CapSet^c1...CapSet^c2, (CapSet^c1) subsumes y ==> X subsumes y * Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y * Contains[X, y] ==> X subsumes y - * - * TODO: Move to CaptureSet */ final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = @@ -239,7 +243,7 @@ trait CaptureRef extends TypeProxy, ValueType: 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. + * @param canAddHidden If true we allow maximal capabilities 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 52a7cd87f647..1c03cb0b12ad 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -636,7 +636,8 @@ object CaptureSet: */ def solve()(using Context): Unit = if !isConst then - val approx = upperApprox(empty).map(Fresh.FromCap(NoSymbol).inverse) + val approx = upperApprox(empty) + .map(Fresh.FromCap(NoSymbol).inverse) // Fresh.Cap --> cap .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems @@ -1139,6 +1140,7 @@ object CaptureSet: /** 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index df6eb2d385cc..738281a6a76e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -519,8 +519,7 @@ class CheckCaptures extends Recheck, SymTransformer: def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => - if sym.exists then - if curEnv.isOpen then markFree(capturedVars(sym), tree) + if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), tree) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType @@ -556,16 +555,21 @@ class CheckCaptures extends Recheck, SymTransformer: if param.isUseParam then markFree(arg.nuType.deepCaptureSet, errTree) end disallowCapInTypeArgs + /** Rechecking idents involves: + * - adding call captures for idents referring to methods + * - marking as free the identifier with any selections or .rd + * modifiers implied by the expected type + */ override def recheckIdent(tree: Ident, pt: Type)(using Context): Type = val sym = tree.symbol if sym.is(Method) then // If ident refers to a parameterless method, charge its cv to the environment includeCallCaptures(sym, sym.info, tree) else if !sym.isStatic then - // Otherwise charge its symbol, but add all selections implied by the e - // expected type `pt`. - // Example: If we have `x` and the expected type says we select that with `.a.b`, - // we charge `x.a.b` instead of `x`. + // Otherwise charge its symbol, but add all selections and also any `.rd` + // modifier implied by the expected type `pt`. + // Example: If we have `x` and the expected type says we select that with `.a.b` + // where `b` is a read-only method, we charge `x.a.b.rd` instead of `x`. def addSelects(ref: TermRef, pt: Type): CaptureRef = pt match case pt: PathSelectionProto if ref.isTracked => if pt.sym.isReadOnlyMethod then @@ -582,7 +586,8 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckIdent(tree, pt) /** The expected type for the qualifier of a selection. If the selection - * could be part of a capabaility path, we return a PathSelectionProto. + * could be part of a capability path or is a a read-only method, we return + * a PathSelectionProto. */ override def selectionProto(tree: Select, pt: Type)(using Context): Type = val sym = tree.symbol @@ -616,6 +621,9 @@ class CheckCaptures extends Recheck, SymTransformer: } case _ => denot + // Don't allow update methods to be called unless the qualifier captures + // contain an exclusive referenece. TODO This should probabkly rolled into + // qualifier logic once we have it. if tree.symbol.isUpdateMethod && !qualType.captureSet.isExclusive then report.error( em"""cannot call update ${tree.symbol} from $qualType, @@ -651,8 +659,8 @@ class CheckCaptures extends Recheck, SymTransformer: selType }//.showing(i"recheck sel $tree, $qualType = $result") - /** Hook for massaging a function before it is applied. Copies all @use annotations - * on method parameter symbols to the corresponding paramInfo types. + /** Hook for massaging a function before it is applied. Copies all @use and @consume + * annotations on method parameter symbols to the corresponding paramInfo types. */ override def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType = val paramInfosWithUses = @@ -682,7 +690,8 @@ class CheckCaptures extends Recheck, SymTransformer: includeCallCaptures(meth, res, tree) res - /** Recheck argument, and, if formal parameter carries a `@use`, + /** Recheck argument against a "freshened" version of `formal` where toplevel `cap` + * occurrences are replaced by `Fresh.Cap`. Also, if formal parameter carries a `@use`, * charge the deep capture set of the actual argument to the environment. */ protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = @@ -773,16 +782,21 @@ class CheckCaptures extends Recheck, SymTransformer: /** First half of result pair: * Refine the type of a constructor call `new C(t_1, ..., t_n)` - * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked - * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. + * to C{val x_1: @refineOverride T_1, ..., x_m: @refineOverride T_m} + * where x_1, ..., x_m are the tracked parameters of C and + * T_1, ..., T_m are the types of the corresponding arguments. The @refineOveride + * annotations avoid problematic intersections of capture sets when those + * parameters are selected. * * Second half: union of initial capture set and all capture sets of arguments - * to tracked parameters. + * to tracked parameters. The initial capture set `initCs` is augmented with + * - Fresh.Cap if `core` extends Mutable + * - Fresh.Cap.rd if `core` extends Capability */ def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = - if core.derivesFromMutable then CaptureSet.fresh() + if core.derivesFromMutable then initCs ++ CaptureSet.fresh() else if core.derivesFromCapability then initCs ++ Fresh.Cap().readOnly.singletonCaptureSet else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do @@ -1488,7 +1502,7 @@ class CheckCaptures extends Recheck, SymTransformer: /** 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 + * The unboxed condition ensures that the expected type is not a type variable * that's upper bounded by a read-only type. In this case it would not be sound * to narrow to the read-only set, since that set can be propagated * by the type variable instantiation. @@ -1514,9 +1528,9 @@ class CheckCaptures extends Recheck, SymTransformer: actual else val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) - val improvedRO = improveReadOnly(improvedVAR, expected) + val improved = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( - improvedRO.withReachCaptures(actual), expected, tree, + improved.withReachCaptures(actual), expected, tree, covariant = true, alwaysConst = false, boxErrors) if adapted eq improvedVAR // no .rd improvement, no box-adaptation then actual // might as well use actual instead of improved widened @@ -1563,17 +1577,19 @@ class CheckCaptures extends Recheck, SymTransformer: /** Check that overrides don't change the @use or @consume 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 def checkAnnot(cls: ClassSymbol) = if param1.hasAnnotation(cls) != param2.hasAnnotation(cls) then - fail(i"has a parameter ${param1.name} with different @${cls.name} status than the corresponding parameter in the overridden definition") + report.error( + OverrideError( + i"has a parameter ${param1.name} with different @${cls.name} status than the corresponding parameter in the overridden definition", + self, member, other, self.memberInfo(member), self.memberInfo(other) + ), + if member.owner == clazz then member.srcPos else clazz.srcPos) + checkAnnot(defn.UseAnnot) checkAnnot(defn.ConsumeAnnot) end OverridingPairsCheckerCC diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 39f6fcf14fd9..f115adfa6421 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -252,7 +252,7 @@ object Existential: tp1.derivedAnnotatedType(toCap(parent), ann) case _ => tp - /** Map existentials at the top-level and in all nested result types to `cap` + /** Map existentials at the top-level and in all nested result types to `Fresh.Cap` */ def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 889f05ce8308..ac275bd660e8 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -16,11 +16,16 @@ import util.SimpleIdentitySet.empty import CaptureSet.{Refs, emptySet, NarrowingCapabilityMap} import dotty.tools.dotc.util.SimpleIdentitySet -/** Handling fresh in CC: - -*/ +/** A module for handling Fresh types. Fresh.Cap instances are top type that keep + * track of what they hide when capabilities get widened by subsumption to fresh. + * The module implements operations to convert between regular caps.cap and + * Fresh.Cap instances. Fresh.Cap is encoded as `caps.cap @freshCapability(...)` where + * `freshCapability(...)` is a special kind of annotation of type `Fresh.Annot` + * that contains a hidden set. + */ object Fresh: + /** The annotation of a Fresh.Cap instance */ case class Annot(hidden: CaptureSet.HiddenSet) extends Annotation: override def symbol(using Context) = defn.FreshCapabilityAnnot override def tree(using Context) = New(symbol.typeRef, Nil) @@ -32,6 +37,9 @@ object Fresh: case _ => false end Annot + /** The initial elements (either 0 or 1) of a hidden set created for given `owner`. + * If owner `x` is a trackable this is `x*` if reach` is true, or `x` otherwise. + */ private def ownerToHidden(owner: Symbol, reach: Boolean)(using Context): Refs = val ref = owner.termRef if reach then @@ -39,6 +47,7 @@ object Fresh: else if ref.isTracked then SimpleIdentitySet(ref) else emptySet + /** An extractor for "fresh" capabilities */ object Cap: def apply(initialHidden: Refs = emptySet)(using Context): CaptureRef = diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 1aee9c467450..608f39b1aa79 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -411,7 +411,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def currentOwner = role.dclSym.orElse(ctx.owner) for hiddenRef <- prune(refsToCheck, tpe, role) do val proot = hiddenRef.pathRootOrShared - if !proot.widen.derivesFrom(defn.Caps_SharedCapability) then + if !proot.widen.derivesFromSharedCapability then proot match case ref: TermRef => val refSym = ref.symbol @@ -448,7 +448,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: role match case _: TypeRole.Argument | _: TypeRole.Qualifier => for ref <- refsToCheck do - if !ref.pathRootOrShared.derivesFrom(defn.Caps_SharedCapability) then + if !ref.pathRootOrShared.derivesFromSharedCapability then consumed.put(ref, pos) case _ => end checkConsumedRefs diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index dd0407c11cfe..86102a172704 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -345,10 +345,18 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: parent case _ => tp + /** Check that types extending SharedCapability don't have a `cap` in their capture set. + * TODO This is not enough. + * We need to also track that we cannot get exclusive capabilities in paths + * where some prefix derives from SharedCapability. Also, can we just + * exclude `cap`, or do we have to extend this to all exclusive capabilties? + * The problem is that we know what is exclusive in general only after capture + * checking, not before. + */ def checkSharedOK(tp: Type): tp.type = tp match case CapturingType(parent, refs) - if refs.isUniversal && parent.derivesFrom(defn.Caps_SharedCapability) => + if refs.isUniversal && parent.derivesFromSharedCapability => fail(em"$tp extends SharedCapability, so it cannot capture `cap`") case _ => tp diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d48ee2c0449e..3fba3dbda082 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1118,8 +1118,6 @@ class Definitions { @tu lazy val SilentAnnots: Set[Symbol] = Set(InlineParamAnnot, ErasedParamAnnot, RefineOverrideAnnot) - @tu lazy val ccParamOnlyAnnotations: Set[Symbol] = Set(UseAnnot, ConsumeAnnot) - // A list of annotations that are commonly used to indicate that a field/method argument or return // type is not null. These annotations are used by the nullification logic in JavaNullInterop to // improve the precision of type nullification. diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index e044d4f09e32..cd90d9f2397a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -606,7 +606,7 @@ 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") + fail(em"Update methods can only be used as members of classes extending the `Mutable` trait") checkApplicable(Erased, !sym.is(Lazy, butNot = Given) && !sym.isMutableVarOrAccessor diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 50497044fee8..4444bdf7e5b3 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -72,15 +72,22 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class use extends annotation.StaticAnnotation + /** An annotations on parameters and update methods. + * On a parameter it states that any capabilties passed in the argument + * are no longer available afterwards, unless they are of class `SharableCapabilitty`. + * On an update method, it states that the `this` of the enclosing class is + * consumed, which means that any capabilities of the method prefix are + * no longer available afterwards. + */ + final class consume extends annotation.StaticAnnotation + /** An annotation placed on a refinement created by capture checking. * Refinements with this annotation unconditionally override any - * info vfrom the parent type, so no intersection needs to be formed. + * info from the parent type, so no intersection needs to be formed. * This could be useful for tracked parameters as well. */ final class refineOverride extends annotation.StaticAnnotation - final class consume extends annotation.StaticAnnotation - object unsafe: extension [T](x: T) diff --git a/project/Build.scala b/project/Build.scala index 6c0d8454ee01..34901a406d2e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1246,7 +1246,7 @@ object Build { settings(scala2LibraryBootstrappedSettings). settings( moduleName := "scala2-library-cc", - scalacOptions += "-Ycheck:all" + scalacOptions += "-Ycheck:all", ) lazy val scala2LibraryBootstrappedSettings = Seq( diff --git a/scala2-library-cc/src/scala/collection/View.scala b/scala2-library-cc/src/scala/collection/View.scala index b30fa5e508fe..482884835cb1 100644 --- a/scala2-library-cc/src/scala/collection/View.scala +++ b/scala2-library-cc/src/scala/collection/View.scala @@ -153,6 +153,9 @@ object View extends IterableFactory[View] { underlying match { case filter: Filter[A]^{underlying} if filter.isFlipped == isFlipped => unsafeAssumeSeparate: + // See filter-iterable.scala for a test where a variant of Filter + // works without the unsafeAssumeSeparate. But it requires significant + // changes compared to the version here. See also Filter in colltest5.CollectionStrawManCC5_1. new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) case _ => new Filter(underlying, p, isFlipped) } diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 3cb57784ad95..f12576033622 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -24,7 +24,7 @@ import scala.language.implicitConversions import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures -import caps.untrackedCaptures +import caps.{cap, untrackedCaptures} import caps.unsafe.unsafeAssumeSeparate /** This class implements an immutable linked list. We call it "lazy" @@ -880,8 +880,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // if cursor (eq scout) has state defined, it is empty; else unknown state if (!cursor.stateDefined) b.append(sep).append("") } else { - @inline def same(a: LazyListIterable[A]^, b: LazyListIterable[A]^): Boolean = (a eq b) || (a.state eq b.state) - // !!!CC with qualifiers, same should have cap.rd parameters + @inline def same(a: LazyListIterable[A]^, b: LazyListIterable[A]^{cap, a}): Boolean = (a eq b) || (a.state eq b.state) // Cycle. // If we have a prefix of length P followed by a cycle of length C, // the scout will be at position (P%C) in the cycle when the cursor @@ -893,7 +892,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // the start of the loop. var runner = this var k = 0 - while (!unsafeAssumeSeparate(same(runner, scout))) { + while (!same(runner, scout)) { runner = runner.tail scout = scout.tail k += 1 @@ -903,11 +902,11 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // everything once. If cursor is already at beginning, we'd better // advance one first unless runner didn't go anywhere (in which case // we've already looped once). - if (unsafeAssumeSeparate(same(cursor, scout)) && (k > 0)) { + if (same(cursor, scout) && (k > 0)) { appendCursorElement() cursor = cursor.tail } - while (!unsafeAssumeSeparate(same(cursor, scout))) { + while (!same(cursor, scout)) { appendCursorElement() cursor = cursor.tail } @@ -1183,10 +1182,10 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { * @param f the function that's repeatedly applied * @return the LazyListIterable returning the infinite sequence of values `start, f(start), f(f(start)), ...` */ - def iterate[A](start: => A)(f: A => A): LazyListIterable[A]^{start, f} = + def iterate[A](start: => A)(f: A ->{cap, start} A): LazyListIterable[A]^{start, f} = newLL { val head = start - sCons(head, unsafeAssumeSeparate(iterate(f(head))(f))) + sCons(head, iterate(f(head))(f)) } /** diff --git a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala index 7bfda7972762..1c3f669f5358 100644 --- a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala +++ b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala @@ -14,10 +14,11 @@ package scala package collection package mutable import language.experimental.captureChecking +import caps.cap private[mutable] trait CheckedIndexedSeqView[+A] extends IndexedSeqView[A] { - protected val mutationCount: () -> Int + protected val mutationCount: () ->{cap.rd} Int override def iterator: Iterator[A]^{this} = new CheckedIndexedSeqView.CheckedIterator(this, mutationCount()) override def reverseIterator: Iterator[A]^{this} = new CheckedIndexedSeqView.CheckedReverseIterator(this, mutationCount()) @@ -42,7 +43,7 @@ private[mutable] object CheckedIndexedSeqView { import IndexedSeqView.SomeIndexedSeqOps @SerialVersionUID(3L) - private[mutable] class CheckedIterator[A](self: IndexedSeqView[A]^, mutationCount: -> Int) + private[mutable] class CheckedIterator[A](self: IndexedSeqView[A]^, mutationCount: ->{cap.rd} Int) extends IndexedSeqView.IndexedSeqViewIterator[A](self) { private[this] val expectedCount = mutationCount override def hasNext: Boolean = { @@ -52,7 +53,7 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A]^, mutationCount: -> Int) + private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A]^, mutationCount: ->{cap.rd} Int) extends IndexedSeqView.IndexedSeqViewReverseIterator[A](self) { private[this] val expectedCount = mutationCount override def hasNext: Boolean = { @@ -62,43 +63,43 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - class Id[+A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int) + class Id[+A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Id(underlying) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Appended[+A](underlying: SomeIndexedSeqOps[A]^, elem: A)(protected val mutationCount: () -> Int) + class Appended[+A](underlying: SomeIndexedSeqOps[A]^, elem: A)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Appended(underlying, elem) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int) + class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Prepended(elem, underlying) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int) + class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Concat[A](prefix, suffix) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int) + class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Take(underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class TakeRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int) + class TakeRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.TakeRight(underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Drop[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int) + class Drop[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Drop[A](underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class DropRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int) + class DropRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.DropRight[A](underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Map[A, B](underlying: SomeIndexedSeqOps[A]^, f: A => B)(protected val mutationCount: () -> Int) + class Map[A, B](underlying: SomeIndexedSeqOps[A]^, f: A => B)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Map(underlying, f) with CheckedIndexedSeqView[B] @SerialVersionUID(3L) - class Reverse[A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int) + class Reverse[A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Reverse[A](underlying) with CheckedIndexedSeqView[A] { override def reverse: IndexedSeqView[A] = underlying match { case x: IndexedSeqView[A] => x @@ -107,7 +108,7 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - class Slice[A](underlying: SomeIndexedSeqOps[A]^, from: Int, until: Int)(protected val mutationCount: () -> Int) + class Slice[A](underlying: SomeIndexedSeqOps[A]^, from: Int, until: Int)(protected val mutationCount: () ->{cap.rd} Int) extends AbstractIndexedSeqView[A] with CheckedIndexedSeqView[A] { protected val lo = from max 0 protected val hi = (until max 0) min underlying.length diff --git a/tests/neg-custom-args/captures/mut-outside-mutable.check b/tests/neg-custom-args/captures/mut-outside-mutable.check index 0407f35745b9..bfc1b5161f0a 100644 --- a/tests/neg-custom-args/captures/mut-outside-mutable.check +++ b/tests/neg-custom-args/captures/mut-outside-mutable.check @@ -1,8 +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 + | Update methods can only be used as members of classes extending the `Mutable` trait -- Error: tests/neg-custom-args/captures/mut-outside-mutable.scala:9:12 ------------------------------------------------ 9 | mut def baz() = 1 // error | ^ - | Update methods can only be used as members of classes deriving from the `Mutable` trait + | Update methods can only be used as members of classes extending the `Mutable` trait diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index e12890a9be9b..54e5f7e2c6fd 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -6,7 +6,6 @@ import scala.reflect.ClassTag import annotation.unchecked.{uncheckedVariance, uncheckedCaptures} import annotation.tailrec import caps.cap -import caps.untrackedCaptures import caps.unsafe.unsafeAssumeSeparate /** A strawman architecture for new collections. It contains some @@ -69,10 +68,7 @@ object CollectionStrawMan5 { /** Base trait for strict collections */ trait Buildable[+A] extends Iterable[A] { protected def newBuilder: Builder[A, Repr] @uncheckedVariance - override def partition(p: A => Boolean): (Repr, Repr) @untrackedCaptures = - // Without untrackedCaptures this fails SepChecks.checkType. - // But this is probably an error in the hiding logic. - // TODO remove @untrackedCaptures and investigate + override def partition(p: A => Boolean): (Repr, Repr) = val l, r = newBuilder iterator.foreach(x => (if (p(x)) l else r) += x) (l.result, r.result) @@ -120,7 +116,7 @@ object CollectionStrawMan5 { this: SeqLike[A] => type C[X] <: Seq[X] def fromIterable[B](coll: Iterable[B]^): C[B] - override protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ ): Repr = + override protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr = fromIterable(coll) trait IterableOps[+A] extends Any { diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index 2b3b27c94243..2bde8cb5a885 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -1,7 +1,6 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag import caps.unsafe.unsafeAssumeSeparate -import language.`3.7` // sepchecks on object Test { import colltest5.strawman.collections.* From d661678b2c9f2a004185026c297a3d5f71dad714 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 1 Feb 2025 20:19:24 +0100 Subject: [PATCH 22/93] Polish and document separation checker. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 1 + .../src/dotty/tools/dotc/cc/SepCheck.scala | 248 +++++++++++++++--- 3 files changed, 219 insertions(+), 32 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 738281a6a76e..a54b90b30060 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1839,7 +1839,7 @@ class CheckCaptures extends Recheck, SymTransformer: end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) - if ccConfig.useSepChecks then SepChecker(this).traverse(unit) + if ccConfig.useSepChecks then SepCheck(this).traverse(unit) if !ctx.reporter.errorsReported then // We dont report errors here if previous errors were reported, because other // errors often result in bad applied types, but flagging these bad types gives diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index ac275bd660e8..bbc14fc9be62 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -67,6 +67,7 @@ object Fresh: case _ => None end Cap + /** Map each occurrence of cap to a different Sep.Cap instance */ class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: thisMap => diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 608f39b1aa79..b039d12abf91 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -14,7 +14,19 @@ import util.{SimpleIdentitySet, EqHashMap, SrcPos} import tpd.* import reflect.ClassTag -object SepChecker: +/** The separation checker is a tree traverser that is run after capture checking. + * It checks tree nodes for various separation conditions, explained in the + * methods below. Rough summary: + * + * - Hidden sets of arguments must not be referred to in the same application + * - Hidden sets of (result-) types must not be referred to alter in the same scope. + * - Returned hidden sets can only refer to @consume parameters. + * - If returned hidden sets refer to an encloding this, the reference must be + * from a @consume method. + * - Consumed entities cannot be used subsequently. + * - Entitites cannot be consumed in a loop. + */ +object SepCheck: /** Enumerates kinds of captures encountered so far */ enum Captures: @@ -50,7 +62,7 @@ object SepChecker: case TypeRole.Argument(_) => "the argument's adapted type" case TypeRole.Qualifier(_, meth) => - i"the type of the qualifier to a call of $meth" + i"the type of the prefix to a call of $meth" end TypeRole /** A class for segmented sets of consumed references. @@ -126,14 +138,13 @@ object SepChecker: else ConstConsumedSet(refs.slice(start, size), locs.slice(start, size)) finally size = start - end MutConsumedSet val EmptyConsumedSet = ConstConsumedSet(Array(), Array()) -class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: +class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import checker.* - import SepChecker.* + import SepCheck.* /** The set of capabilities that are hidden by a polymorphic result type * of some previous definition. @@ -150,17 +161,29 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: */ private var previousDefs: List[mutable.ListBuffer[ValOrDefDef]] = Nil + /** The set of references that were consumed so far in the current method */ private var consumed: MutConsumedSet = MutConsumedSet() + /** Run `op`` with a fresh, initially empty consumed set. */ private def withFreshConsumed(op: => Unit): Unit = val saved = consumed consumed = MutConsumedSet() op consumed = saved + /** Infos aboput Labeled expressions enclosing the current traversal point. + * For each labeled expression, it's label name, and a list buffer containing + * all consumed sets of return expressions referring to that label. + */ private var openLabeled: List[(Name, mutable.ListBuffer[ConsumedSet])] = Nil extension (refs: Refs) + + /** The footprint of a set of references `refs` the smallest set `F` such that + * - no maximal capability is in `F` + * - all non-maximal capabilities in `refs` are in `F` + * - if `f in F` then the footprint of `f`'s info is also in `F`. + */ private def footprint(using Context): Refs = def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match case newElem :: newElems1 => @@ -171,6 +194,18 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val elems: Refs = refs.filter(!_.isMaxCapability) recur(elems, elems.toList) + /** The overlap of two footprint sets F1 and F2. This contains all exclusive references `r` + * such that one of the following is true: + * 1. + * - one of the sets contains `r` + * - the other contains a capability `s` or `s.rd` where `s` _covers_ `r` + * 2. + * - one of the sets contains `r.rd` + * - the other contains a capability `s` where `s` _covers_ `r` + * + * A capability `s` covers `r` if `r` can be seen as a path extension of `s`. E.g. + * if `s = x.a` and `r = x.a.b.c` then `s` covers `a`. + */ private def overlapWith(other: Refs)(using Context): Refs = val refs1 = refs val refs2 = other @@ -182,7 +217,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: */ def common(refs1: Refs, refs2: Refs) = refs1.filter: ref => - ref.isExclusive && refs2.exists(ref2 => ref2.stripReadOnly.covers(ref)) + ref.isExclusive && refs2.exists(_.stripReadOnly.covers(ref)) ++ refs1 .filter: @@ -190,7 +225,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: // We can get away testing only references with at least one field selection // here since stripped readOnly references that equal a reference in refs2 // are added by the first clause of the symmetric call to common. - !ref.isCap && refs2.exists(ref2 => ref2.covers(prefix)) + !ref.isCap && refs2.exists(_.covers(prefix)) case _ => false .map(_.stripReadOnly) @@ -198,19 +233,40 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: common(refs, other) ++ common(other, refs) end overlapWith + /** The non-maximal elements hidden directly or indirectly by a maximal + * capability in `refs`. E g. if `R = {x, >}` then + * its hidden set is `{y, z}`. + */ private def hidden(using Context): Refs = val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet - def recur(cs: Refs): Refs = - (emptySet /: cs): (elems, elem) => - if seen.add(elem) then elems ++ hiddenByElem(elem, recur) - else elems + + def hiddenByElem(elem: CaptureRef): Refs = elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) + case ReadOnlyCapability(ref1) => hiddenByElem(ref1).map(_.readOnly) + case _ => emptySet + + def recur(refs: Refs): Refs = + (emptySet /: refs): (elems, elem) => + if seen.add(elem) then elems ++ hiddenByElem(elem) else elems + recur(refs) end hidden + /** Same as !refs.hidden.isEmpty but more efficient */ private def containsHidden(using Context): Boolean = - refs.exists: ref => - !hiddenByElem(ref, _ => emptySet).isEmpty + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + def recur(refs: Refs): Boolean = refs.exists: ref => + seen.add(ref) && ref.stripReadOnly.match + case Fresh.Cap(hcs) => + hcs.elems.exists(!_.isRootCapability) || recur(hcs.elems) + case _ => + false + + recur(refs) + end containsHidden + + /** Subtract all elements that are covered by some element in `others` from this set. */ private def deduct(others: Refs)(using Context): Refs = refs.filter: ref => !others.exists(_.covers(ref)) @@ -221,27 +277,36 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if ref.isTrackableRef then refs.deduct(CaptureSet(ref, ref.reach).elems.footprint) else refs - /** Deduct the footprint of all captures of `deps` from `refs` */ + /** Deduct the footprint of all captures of trees in `deps` from `refs` */ private def deductCapturesOf(deps: List[Tree])(using Context): Refs = deps.foldLeft(refs): (refs, dep) => refs.deduct(captures(dep).footprint) end extension - private def hiddenByElem(ref: CaptureRef, recur: Refs => Refs)(using Context): Refs = ref match - case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) - case ReadOnlyCapability(ref1) => hiddenByElem(ref1, recur).map(_.readOnly) - case _ => emptySet - - /** The captures of an argument or prefix widened to the formal parameter, if + /** The deep capture set of an argument or prefix widened to the formal parameter, if * the latter contains a cap. */ private def formalCaptures(arg: Tree)(using Context): Refs = arg.formalType.orElse(arg.nuType).deepCaptureSet.elems - /** The captures of a node */ + /** The deep capture set if the type of `tree` */ private def captures(tree: Tree)(using Context): Refs = tree.nuType.deepCaptureSet.elems + // ---- Error reporting TODO Once these are stabilized, move to messages ----- + + /** Report a separation failure in an application `fn(args)` + * @param fn the function + * @param args the flattened argument lists + * @param argIdx the index of the failing argument in `args`, starting at 0 + * @param overlap the overlap causing the failure + * @param hiddenInArg the hidxden set of the type of the failing argument + * @param footprints a sequence of partial footprints, and the index of the + * last argument they cover. + * @param deps cross argument dependencies: maps argument trees to + * those other arguments that where mentioned by coorresponding + * formal parameters. + */ private def sepApplyError(fn: Tree, args: List[Tree], argIdx: Int, overlap: Refs, hiddenInArg: Refs, footprints: List[(Refs, Int)], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = @@ -302,6 +367,13 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: arg.srcPos) end sepApplyError + /** Report a use/definition failure, where a previously hidden capability is + * used again. + * @param tree the tree where the capability is used + * @param used the footprint of all uses of `tree` + * @param globalOverlap the overlap between `used` and all capabilities hidden + * by previous definitions + */ def sepUseError(tree: Tree, used: Refs, globalOverlap: Refs)(using Context): Unit = val individualChecks = for mdefs <- previousDefs.iterator; mdef <- mdefs.iterator yield val hiddenByDef = captures(mdef.tpt).hidden.footprint @@ -323,6 +395,11 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: tree.srcPos) end sepUseError + /** Report a failure where a previously consumed capability is used again, + * @param ref the capability that is used after being consumed + * @param loc the position where the capability was consumed + * @param pos the position where the capability was used again + */ def consumeError(ref: CaptureRef, loc: SrcPos, pos: SrcPos)(using Context): Unit = report.error( em"""Separation failure: Illegal access to $ref, which was passed to a @@ -330,12 +407,43 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: |and therefore is no longer available.""", pos) + /** Report a failure where a capability is consumed in a loop. + * @param ref the capability + * @param loc the position where the capability was consumed + */ def consumeInLoopError(ref: CaptureRef, pos: SrcPos)(using Context): Unit = report.error( em"""Separation failure: $ref appears in a loop, therefore it cannot |be passed to a @consume parameter or be used as a prefix of a @consume method call.""", pos) + // ------------ Checks ----------------------------------------------------- + + /** Check separation between different arguments and between function + * prefix and arguments. A capability cannot be hidden by one of these arguments + * and also be either explicitly referenced or hidden by the prefix or another + * argument. "Hidden" means: the capability is in the deep capture set of the + * argument and appears in the hidden set of the corresponding (capture-polymorphic) + * formal parameter. Howeber, we do allow explicit references to a hidden + * capability in later arguments, if the corresponding formal parameter mentions + * the parameter where the capability was hidden. For instance in + * + * def seq(x: () => Unit; y ->{cap, x} Unit): Unit + * def f: () ->{io} Unit + * + * we do allow `seq(f, f)` even though `{f, io}` is in the hidden set of the + * first parameter `x`, since the second parameter explicitly mentions `x` in + * its capture set. + * + * Also check separation via checkType within individual arguments widened to their + * formal paramater types. + * + * @param fn the applied function + * @param args the flattened argument lists + * @param deps cross argument dependencies: maps argument trees to + * those other arguments that where mentioned by coorresponding + * formal parameters. + */ private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = val fnCaptures = methPart(fn) match case Select(qual, _) => qual.nuType.captureSet @@ -345,10 +453,18 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val footprints = mutable.ListBuffer[(Refs, Int)]((footprint, 0)) val indexedArgs = args.zipWithIndex + // First, compute all footprints of arguments to monomorphic pararameters, + // separately in `footprints`, and their union in `footprint`. for (arg, idx) <- indexedArgs do if !arg.needsSepCheck then footprint = footprint ++ captures(arg).footprint.deductCapturesOf(deps(arg)) footprints += ((footprint, idx + 1)) + + // Then, for each argument to a polymorphic parameter: + // - check formal type via checkType + // - check that hidden set of argument does not overlap with current footprint + // - add footprint of the deep capture set of actual type of argument + // to global footprint(s) for (arg, idx) <- indexedArgs do if arg.needsSepCheck then val ac = formalCaptures(arg) @@ -362,6 +478,10 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: footprints += ((footprint, idx + 1)) end checkApply + /** The def/use overlap between the references `hiddenByDef` hidden by + * a previous definition and the `used` set of a tree with symbol `sym`. + * Deduct any capabilities referred to or hidden by the (result-) type of `sym`. + */ def defUseOverlap(hiddenByDef: Refs, used: Refs, sym: Symbol)(using Context): Refs = val overlap = hiddenByDef.overlapWith(used) resultType.get(sym) match @@ -371,6 +491,10 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case _ => overlap + /** 1. Check that the capabilities used at `tree` don't overlap with + * capabilities hidden by a previous definition. + * 2. Also check that none of the used capabilities was consumed before. + */ def checkUse(tree: Tree)(using Context) = val used = tree.markedFree if !used.elems.isEmpty then @@ -382,6 +506,9 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val pos = consumed.get(ref) if pos != null then consumeError(ref, pos, tree.srcPos) + /** If `tp` denotes some version of a singleton type `x.type` the set `{x}` + * otherwise the empty set. + */ def explicitRefs(tp: Type): Refs = tp match case tp: (TermRef | ThisType) => SimpleIdentitySet(tp) case AnnotatedType(parent, _) => explicitRefs(parent) @@ -389,17 +516,31 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case OrType(tp1, tp2) => explicitRefs(tp1) ** explicitRefs(tp2) case _ => emptySet + /** Deduct some elements from `refs` according to the role of the checked type `tpe`: + * - If the the type apears as a (result-) type of a definition of `x`, deduct + * `x` and `x*`. + * - If `tpe` is morally a singleton type deduct it as well. + */ def prune(refs: Refs, tpe: Type, role: TypeRole)(using Context): Refs = refs.deductSym(role.dclSym).deduct(explicitRefs(tpe)) - def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = - checkType(tpt.nuType, tpt.srcPos, - TypeRole.Result(sym, inferred = tpt.isInstanceOf[InferredTypeTree])) - - /** Check validity consumed references `refsToCheck`. The references are consumed + /** Check validity of consumed references `refsToCheck`. The references are consumed * because they are hidden in a Fresh.Cap result type or they are referred * to in an argument to a @consume parameter or in a prefix of a @consume method -- - * which one applie is determined by the role parameter. + * which one applies is determined by the role parameter. + * + * This entails the following checks: + * - The reference must be defined in the same as method or class as + * the access. + * - If the reference is to a term parameter, that parameter must be + * marked as @consume as well. + * - If the reference is to a this type of the enclosing class, the + * access must be in a @consume method. + * + * References that extend SharedCapability are excluded from checking. + * As a side effect, add all checked references with the given position `pos` + * to the global `consumed` map. + * * @param refsToCheck the referencves to check * @param tpe the type containing those references * @param role the role in which the type apears @@ -453,9 +594,18 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case _ => end checkConsumedRefs - /** Check that all parts of type `tpe` are separated. */ + /** Check separation conditions of type `tpe` that appears in `role`. + * 1. Check that the parts of type `tpe` are mutually separated, as defined in + * `checkParts` below. + * 2. Check that validity of all references consumed by the type as defined in + * `checkLegalRefs` below + */ def checkType(tpe: Type, pos: SrcPos, role: TypeRole)(using Context): Unit = + /** Check that the parts of type `tpe` are mutually separated. + * This means that references hidden in some part of the type may not + * be explicitly referenced or hidden in some other part. + */ def checkParts(parts: List[Type]): Unit = var footprint: Refs = emptySet var hiddenSet: Refs = emptySet @@ -502,11 +652,15 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: end for end checkParts + /** A traverser that collects part lists to check for separation conditions. + * The accumulator of type `Captures` indicates what kind of captures were + * encountered in previous parts. + */ object traverse extends TypeAccumulator[Captures]: /** A stack of part lists to check. We maintain this since immediately - * checking parts when traversing the type would check innermost to oputermost. - * But we want to check outermost parts first since this prioritized errors + * checking parts when traversing the type would check innermost to outermost. + * But we want to check outermost parts first since this prioritizes errors * that are more obvious. */ var toCheck: List[List[Type]] = Nil @@ -536,6 +690,11 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case t => foldOver(c, t) + /** If `tpe` appears as a (result-) type of a definition, treat its + * hidden set minus its explicitly declared footprint as consumed. + * If `tpe` appears as an argument to a @consume parameter, treat + * its footprint as consumed. + */ def checkLegalRefs() = role match case TypeRole.Result(sym, _) => if !sym.isAnonymousFunction // we don't check return types of anonymous functions @@ -559,11 +718,27 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: checkLegalRefs() end checkType + /** Check the (result-) type of a definition of symbol `sym` */ + def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = + checkType(tpt.nuType, tpt.srcPos, + TypeRole.Result(sym, inferred = tpt.isInstanceOf[InferredTypeTree])) + + /** The list of all individual method types making up some potentially + * curried method type. + */ private def collectMethodTypes(tp: Type): List[TermLambda] = tp match case tp: MethodType => tp :: collectMethodTypes(tp.resType) case tp: PolyType => collectMethodTypes(tp.resType) case _ => Nil + /** The inter-parameter dependencies of the function reference `fn` applied + * to the argument lists `argss`. For instance, if `f` has type + * + * f(x: A, y: B^{cap, x}, z: C^{x, y}): D + * + * then the dependencies of an application `f(a, b)` is a map that takes + * `b` to `List(a)` and `c` to `List(a, b)`. + */ private def dependencies(fn: Tree, argss: List[List[Tree]])(using Context): collection.Map[Tree, List[Tree]] = val mtpe = if fn.symbol.exists then fn.symbol.info @@ -589,6 +764,10 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: deps(arg) ++= referred deps + /** Decompose an application into a function prefix and a list of argument lists. + * If some of the arguments need a separation check because they are capture polymorphic, + * perform a separation check with `checkApply` + */ private def traverseApply(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 @@ -596,10 +775,16 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if argss.nestedExists(_.needsSepCheck) then checkApply(tree, argss.flatten, dependencies(tree, argss)) + /** Is `tree` an application of `caps.unsafe.unsafeAssumeSeparate`? */ def isUnsafeAssumeSeparate(tree: Tree)(using Context): Boolean = tree match case tree: Apply => tree.symbol == defn.Caps_unsafeAssumeSeparate case _ => false + /** Check (result-) type of `tree` for separation conditions using `checkType`. + * Excluded are parameters and definitions that have an =unsafeAssumeSeparate + * application as right hand sides. + * Hidden sets of checked definitions are added to `defsShadow`. + */ def checkValOrDefDef(tree: ValOrDefDef)(using Context): Unit = if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then checkType(tree.tpt, tree.symbol) @@ -609,6 +794,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: resultType(tree.symbol) = tree.tpt.nuType previousDefs.head += tree + /** Traverse `tree` and perform separation checks everywhere */ def traverse(tree: Tree)(using Context): Unit = if isUnsafeAssumeSeparate(tree) then return checkUse(tree) @@ -674,4 +860,4 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: consumeInLoopError(ref, pos) case _ => traverseChildren(tree) -end SepChecker \ No newline at end of file +end SepCheck \ No newline at end of file From 97108728ff6a5af064c10deb8eeb60964f477df7 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Feb 2025 09:53:24 +0100 Subject: [PATCH 23/93] Address review comments --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 ++-- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 6 ++--- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 24 ++++++++----------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 8 +++---- .../src/dotty/tools/dotc/cc/SepCheck.scala | 16 ++++++------- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 25516da531fe..349711ef21b0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -508,7 +508,7 @@ extension (tp: Type) case _ => ReadOnlyCapability(tp) - /** If `x` is a capture ref, replacxe all no-flip covariant occurrences of `cap` + /** If `x` is a capture ref, replace all no-flip covariant occurrences of `cap` * in type `tp` with `x*`. */ def withReachCaptures(ref: Type)(using Context): Type = @@ -758,7 +758,7 @@ object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot): protected def unwrappable(using Context) = Set() /** An extractor for `ref @readOnlyCapability`, which is used to express - * the rad-only capability `ref.rd` as a type. + * the read-only capability `ref.rd` as a type. */ object ReadOnlyCapability extends AnnotatedCapability(defn.ReadOnlyCapabilityAnnot): protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 9987b0e91121..a2ceb1f20372 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -116,13 +116,13 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false /** An exclusive capability is a capability that derives - * indirectly from a maximal capability without goinh through + * indirectly from a maximal capability without going through * a read-only capability first. */ 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. + // With the support of paths, we don't need to normalize the `TermRef`s anymore. // /** Normalize reference so that it can be compared with `eq` for equality */ // final def normalizedRef(using Context): CaptureRef = this match // case tp @ AnnotatedType(parent: CaptureRef, annot) if tp.isTrackableRef => @@ -242,7 +242,7 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false end subsumes - /** This is a maximal capabaility that subsumes `y` in given context and VarState. + /** This is a maximal capability that subsumes `y` in given context and VarState. * @param canAddHidden If true we allow maximal capabilities to subsume all other capabilities. * We add those capabilities to the hidden set if this is Fresh.Cap * If false we only accept `y` elements that are already in the diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 1c03cb0b12ad..55fda0f22a08 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -227,11 +227,7 @@ sealed abstract class CaptureSet extends Showable: elems.forall(that.mightAccountFor) && !that.elems.forall(this.mightAccountFor) - /** The subcapturing test. - * @param frozen if true, no new variables or dependent sets are allowed to - * be added when making this test. An attempt to add either - * will result in failure. - */ + /** The subcapturing test, taking an explicit VarState. */ final def subCaptures(that: CaptureSet, vs: VarState)(using Context): CompareResult = subCaptures(that)(using ctx, vs) @@ -392,7 +388,7 @@ 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. + /** Apply function `f` to the elements. Typically used for printing. * Overridden in HiddenSet so that we don't run into infinite recursions */ def processElems[T](f: Refs => T): T = f(elems) @@ -407,10 +403,10 @@ object CaptureSet: /** If set to `true`, capture stack traces that tell us where sets are created */ private final val debugSets = false - val emptySet = SimpleIdentitySet.empty + val emptyRefs: Refs = SimpleIdentitySet.empty /** The empty capture set `{}` */ - val empty: CaptureSet.Const = Const(emptySet) + val empty: CaptureSet.Const = Const(emptyRefs) /** The universal capture set `{cap}` */ def universal(using Context): CaptureSet = @@ -466,7 +462,7 @@ object CaptureSet: * nulls, this provides more lenient checking against compilation units that * were not yet compiled with capture checking on. */ - object Fluid extends Const(emptySet): + object Fluid extends Const(emptyRefs): override def isAlwaysEmpty = false override def addThisElem(elem: CaptureRef)(using Context, VarState) = CompareResult.OK override def accountsFor(x: CaptureRef)(using Context, VarState): Boolean = true @@ -475,7 +471,7 @@ object CaptureSet: end Fluid /** The subclass of captureset variables with given initial elements */ - class Var(override val owner: Symbol = NoSymbol, initialElems: Refs = emptySet, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: + class Var(override val owner: Symbol = NoSymbol, initialElems: Refs = emptyRefs, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: /** A unique identification number for diagnostics */ val id = @@ -493,7 +489,7 @@ object CaptureSet: /** The sets currently known to be dependent sets (i.e. new additions to this set * are propagated to these dependent sets.) */ - var deps: Deps = emptySet + var deps: Deps = SimpleIdentitySet.empty def isConst = isSolved def isAlwaysEmpty = isSolved && elems.isEmpty @@ -927,16 +923,16 @@ object CaptureSet: 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) + class HiddenSet(initialHidden: Refs = emptyRefs)(using @constructorOnly ictx: Context) extends Var(initialElems = initialHidden): /** Apply function `f` to `elems` while setting `elems` to empty for the - * duration. This is used to escape infinite recursions if two Frash.Caps + * duration. This is used to escape infinite recursions if two Fresh.Caps * refer to each other in their hidden sets. */ override def processElems[T](f: Refs => T): T = val savedElems = elems - elems = emptySet + elems = emptyRefs try f(savedElems) finally elems = savedElems end HiddenSet diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a54b90b30060..7380996b3aed 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -622,7 +622,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => denot // Don't allow update methods to be called unless the qualifier captures - // contain an exclusive referenece. TODO This should probabkly rolled into + // an exclusive reference. TODO This should probably rolled into // qualifier logic once we have it. if tree.symbol.isUpdateMethod && !qualType.captureSet.isExclusive then report.error( diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index bbc14fc9be62..48b20f18f027 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -13,7 +13,7 @@ import NameKinds.ExistentialBinderName import NameOps.isImpureFunction import reporting.Message import util.SimpleIdentitySet.empty -import CaptureSet.{Refs, emptySet, NarrowingCapabilityMap} +import CaptureSet.{Refs, emptyRefs, NarrowingCapabilityMap} import dotty.tools.dotc.util.SimpleIdentitySet /** A module for handling Fresh types. Fresh.Cap instances are top type that keep @@ -43,14 +43,14 @@ object Fresh: 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 + if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptyRefs else - if ref.isTracked then SimpleIdentitySet(ref) else emptySet + if ref.isTracked then SimpleIdentitySet(ref) else emptyRefs /** An extractor for "fresh" capabilities */ object Cap: - def apply(initialHidden: Refs = emptySet)(using Context): CaptureRef = + def apply(initialHidden: Refs = emptyRefs)(using Context): CaptureRef = if ccConfig.useSepChecks then AnnotatedType(defn.captureRoot.termRef, Annot(CaptureSet.HiddenSet(initialHidden))) else diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index b039d12abf91..e20fe65be405 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -7,7 +7,7 @@ import collection.mutable import core.* import Symbols.*, Types.*, Flags.* import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* -import CaptureSet.{Refs, emptySet, HiddenSet} +import CaptureSet.{Refs, emptyRefs, HiddenSet} import config.Printers.capt import StdNames.nme import util.{SimpleIdentitySet, EqHashMap, SrcPos} @@ -149,7 +149,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: /** The set of capabilities that are hidden by a polymorphic result type * of some previous definition. */ - private var defsShadow: Refs = emptySet + private var defsShadow: Refs = emptyRefs /** A map from definitions to their internal result types. * Populated during separation checking traversal. @@ -243,10 +243,10 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def hiddenByElem(elem: CaptureRef): Refs = elem match case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) case ReadOnlyCapability(ref1) => hiddenByElem(ref1).map(_.readOnly) - case _ => emptySet + case _ => emptyRefs def recur(refs: Refs): Refs = - (emptySet /: refs): (elems, elem) => + (emptyRefs /: refs): (elems, elem) => if seen.add(elem) then elems ++ hiddenByElem(elem) else elems recur(refs) @@ -349,7 +349,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def hiddenCaptures = formalCaptures(arg).hidden def clashFootprint = clashCaptures.footprint def hiddenFootprint = hiddenCaptures.footprint - def declaredFootprint = deps(arg).map(captures(_)).foldLeft(emptySet)(_ ++ _).footprint + def declaredFootprint = deps(arg).map(captures(_)).foldLeft(emptyRefs)(_ ++ _).footprint def footprintOverlap = hiddenFootprint.overlapWith(clashFootprint).deduct(declaredFootprint) report.error( em"""Separation failure: argument of type ${arg.nuType} @@ -514,7 +514,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case AnnotatedType(parent, _) => explicitRefs(parent) case AndType(tp1, tp2) => explicitRefs(tp1) ++ explicitRefs(tp2) case OrType(tp1, tp2) => explicitRefs(tp1) ** explicitRefs(tp2) - case _ => emptySet + case _ => emptyRefs /** Deduct some elements from `refs` according to the role of the checked type `tpe`: * - If the the type apears as a (result-) type of a definition of `x`, deduct @@ -607,8 +607,8 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * be explicitly referenced or hidden in some other part. */ def checkParts(parts: List[Type]): Unit = - var footprint: Refs = emptySet - var hiddenSet: Refs = emptySet + var footprint: Refs = emptyRefs + var hiddenSet: Refs = emptyRefs var checked = 0 for part <- parts do From 367f66cbba5914ea37ce03a9be4345d05413bc71 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 9 Feb 2025 17:05:40 +0100 Subject: [PATCH 24/93] Fix handling paths extending SharedCapabiolity This partially reverts commit 7b3d3f4eb25ba1d169392ee8f6cbf6a0082a46d7. It looks like this fixes the problems we had with CI timeouts as well. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 14 ++------------ compiler/src/dotty/tools/dotc/cc/SepCheck.scala | 7 +++---- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 349711ef21b0..6fde983b9a5c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -285,20 +285,10 @@ extension (tp: Type) * are of the form this.C but their pathroot is still this.C, not this. */ final def pathRoot(using Context): Type = tp.dealias match - case tp1: TermRef if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot - case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot + case tp1: NamedType if tp1.symbol.maybeOwner.isClass && !tp1.symbol.is(TypeParam) => + tp1.prefix.pathRoot case tp1 => tp1 - /** The first element of a path type, but stop at references extending - * SharedCapability. - */ - final def pathRootOrShared(using Context): Type = - if tp.derivesFromSharedCapability then tp - else tp.dealias match - case tp1: TermRef if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot - case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot - case tp1 => tp1 - /** If this part starts with `C.this`, the class `C`. * Otherwise, if it starts with a reference `r`, `r`'s owner. * Otherwise NoSymbol. diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index e20fe65be405..39f999fa6084 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -551,9 +551,8 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val badParams = mutable.ListBuffer[Symbol]() def currentOwner = role.dclSym.orElse(ctx.owner) for hiddenRef <- prune(refsToCheck, tpe, role) do - val proot = hiddenRef.pathRootOrShared - if !proot.widen.derivesFromSharedCapability then - proot match + if !hiddenRef.derivesFromSharedCapability then + hiddenRef.pathRoot match case ref: TermRef => val refSym = ref.symbol if currentOwner.enclosingMethodOrClass.isProperlyContainedIn(refSym.maybeOwner.enclosingMethodOrClass) then @@ -589,7 +588,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: role match case _: TypeRole.Argument | _: TypeRole.Qualifier => for ref <- refsToCheck do - if !ref.pathRootOrShared.derivesFromSharedCapability then + if !ref.derivesFromSharedCapability then consumed.put(ref, pos) case _ => end checkConsumedRefs From 51050f9d3f96b781b03f7d03ba7d5857371a75dd Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 9 Feb 2025 17:11:19 +0100 Subject: [PATCH 25/93] Streamline deepCaptureSet --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 6fde983b9a5c..7692cf191bf9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -232,9 +232,7 @@ extension (tp: Type) case tp @ ReachCapability(_) => tp.singletonCaptureSet case ReadOnlyCapability(ref) => - val refDcs = ref.deepCaptureSet(includeTypevars) - if refDcs.isConst then CaptureSet(refDcs.elems.map(_.readOnly)) - else refDcs // this case should not happen for correct programs + ref.deepCaptureSet(includeTypevars).readOnly case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet case _ => From f0e0117f6464e6636e6ca6811c7d80015dc43660 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 2 Feb 2025 19:48:04 +0100 Subject: [PATCH 26/93] Avoid edge case where non-sensical info was printed for selections --- compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 372b2ac095d1..a6be756738eb 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -940,7 +940,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { tree.hasType && tree.symbol.exists && ctx.settings.YprintSyms.value protected def nameIdText[T <: Untyped](tree: NameTree[T]): Text = - if (tree.hasType && tree.symbol.exists) { + if (tree.hasType && tree.symbol.exists && tree.symbol.isType == tree.name.isTypeName) { val str = nameString(tree.symbol) tree match { case tree: RefTree => withPos(str, tree.sourcePos) From 8d15e888b15c8e28f2fa777a26cbc6c02f27608a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 3 Feb 2025 13:50:19 +0100 Subject: [PATCH 27/93] Make printFresh a -Y option --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 1 + compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index c6c0ab47de52..5477767f6ba9 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -452,6 +452,7 @@ private sealed trait YSettings: val YccDebug: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references.") val YccNew: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-new", "Used in conjunction with captureChecking language import, try out new variants (debug option)") val YccLog: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-log", "Used in conjunction with captureChecking language import, print tracing and debug info") + val YccPrintFresh: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-print-fresh", "Print hidden sets of fresh `cap` instances") val YccPrintSetup: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-print-setup", "Used in conjunction with captureChecking language import, print trees after cc.Setup phase") /** Area-specific debug output */ diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 3f086b28656c..d7560c96bd9a 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -28,7 +28,7 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def printDebug = ctx.settings.YprintDebug.value /** Print Fresh.Cap instances as */ - protected def printFreshDetailed = printDebug + protected def printFreshDetailed = ctx.settings.YccPrintFresh.value /** Print Fresh.Cap instances as "fresh" */ protected def printFresh = printFreshDetailed || ctx.property(PrintFresh).isDefined From 5132d55ffecc482138c6e03f8d6ec64af32216d2 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 3 Feb 2025 15:05:06 +0100 Subject: [PATCH 28/93] Charge deep capture set for arguments to @consume parameters --- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 4 ++-- .../captures/unsound-reach-6.check | 17 ++++++++++++++++- .../captures/unsound-reach-6.scala | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 7380996b3aed..12f8e26dff82 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -698,8 +698,8 @@ class CheckCaptures extends Recheck, SymTransformer: val freshenedFormal = Fresh.fromCap(formal) val argType = recheck(arg, freshenedFormal) .showing(i"recheck arg $arg vs $freshenedFormal", capt) - if formal.hasAnnotation(defn.UseAnnot) then - // The @use annotation is added to `formal` by `prepareFunction` + if formal.hasAnnotation(defn.UseAnnot) || formal.hasAnnotation(defn.ConsumeAnnot) then + // The @use and/or @consume annotation is added to `formal` by `prepareFunction` capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") markFree(argType.deepCaptureSet, arg) if formal.containsCap then diff --git a/tests/neg-custom-args/captures/unsound-reach-6.check b/tests/neg-custom-args/captures/unsound-reach-6.check index 90fd6c40fbfd..ed81271efa90 100644 --- a/tests/neg-custom-args/captures/unsound-reach-6.check +++ b/tests/neg-custom-args/captures/unsound-reach-6.check @@ -6,7 +6,22 @@ -- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:11:14 --------------------------------------------------- 11 | val z = f(ys) // error @consume failure | ^^ - |Separation failure: argument to @consume parameter with type (ys : List[box () ->{io} Unit]) refers to non-local parameter ys + | Local reach capability ys* leaks into capture scope of method test. + | To allow this, the parameter ys should be declared with a @use annotation +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-6.scala:13:22 ------------------------------ +13 | val _: () -> Unit = x // error + | ^ + | Found: (x : () ->{ys*} Unit) + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-6.scala:21:22 ------------------------------ +21 | val _: () -> Unit = x // error + | ^ + | Found: (x : () ->{io} Unit) + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:19:14 --------------------------------------------------- 19 | val z = f(ys) // error @consume failure | ^^ diff --git a/tests/neg-custom-args/captures/unsound-reach-6.scala b/tests/neg-custom-args/captures/unsound-reach-6.scala index 4ce789025837..4643950d78d0 100644 --- a/tests/neg-custom-args/captures/unsound-reach-6.scala +++ b/tests/neg-custom-args/captures/unsound-reach-6.scala @@ -10,7 +10,7 @@ def test(io: IO^)(ys: List[() ->{io} Unit]) = val x = () => val z = f(ys) // error @consume failure z() - val _: () -> Unit = x // !!! ys* gets lost + val _: () -> Unit = x // error () def test(io: IO^) = @@ -18,7 +18,7 @@ def test(io: IO^) = val x = () => val z = f(ys) // error @consume failure z() - val _: () -> Unit = x // !!! io gets lost + val _: () -> Unit = x // error () From 21a5399fa9acda33518399a90d7ae6c65254eb89 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 3 Feb 2025 15:35:13 +0100 Subject: [PATCH 29/93] Don't flag local fresh capabilities as errors in markFree --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 12 ++++++-- .../captures/delayedRunops.check | 4 --- .../captures/delayedRunops.scala | 2 +- tests/neg-custom-args/captures/i21401.check | 4 --- tests/neg-custom-args/captures/i21401.scala | 2 +- tests/neg-custom-args/captures/i21442.check | 4 --- tests/neg-custom-args/captures/i21442.scala | 2 +- .../captures/leak-problem.scala | 2 +- tests/neg-custom-args/captures/path-use.check | 4 --- tests/neg-custom-args/captures/path-use.scala | 25 ----------------- tests/neg-custom-args/captures/reaches.check | 4 --- tests/neg-custom-args/captures/reaches.scala | 2 +- .../captures/unsound-reach-4.check | 4 --- .../captures/unsound-reach-4.scala | 2 +- .../captures/unsound-reach.check | 5 ---- .../captures/unsound-reach.scala | 2 +- .../pos-custom-args/captures/sep-pairs.scala | 28 +++++++++++++++++++ tests/pos-custom-args/captures/path-use.scala | 21 ++++++++------ 18 files changed, 56 insertions(+), 73 deletions(-) delete mode 100644 tests/neg-custom-args/captures/path-use.check delete mode 100644 tests/neg-custom-args/captures/path-use.scala create mode 100644 tests/pending/pos-custom-args/captures/sep-pairs.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 12f8e26dff82..1513d4422e2f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -486,9 +486,15 @@ class CheckCaptures extends Recheck, SymTransformer: // The path-use.scala neg test contains an example. val underlying = CaptureSet.ofTypeDeeply(c1.widen) capt.println(i"Widen reach $c to $underlying in ${env.owner}") - underlying.disallowRootCapability: () => - report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", tree.srcPos) - recur(underlying, env, null) + if ccConfig.useSepChecks then + recur(underlying.filter(!_.isMaxCapability), env, null) + // we don't want to disallow underlying Fresh.Cap, since these are typically locally created + // fresh capabilities. We don't need to also follow the hidden set since separation + // checking makes ure that locally hidden references need to go to @consume parameters. + else + underlying.disallowRootCapability: () => + report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", tree.srcPos) + recur(underlying, env, null) case c: TypeRef if c.isParamPath => checkUseDeclared(c, env, null) case _ => diff --git a/tests/neg-custom-args/captures/delayedRunops.check b/tests/neg-custom-args/captures/delayedRunops.check index 372e010d191a..14ecbcffd8dd 100644 --- a/tests/neg-custom-args/captures/delayedRunops.check +++ b/tests/neg-custom-args/captures/delayedRunops.check @@ -3,10 +3,6 @@ | ^^^^ | reference ops* is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Unit --- Error: tests/neg-custom-args/captures/delayedRunops.scala:23:13 ----------------------------------------------------- -23 | runOps(ops1) // error - | ^^^^ - | Local reach capability ops1* leaks into capture scope of enclosing function -- Error: tests/neg-custom-args/captures/delayedRunops.scala:29:13 ----------------------------------------------------- 29 | runOps(ops1) // error | ^^^^ diff --git a/tests/neg-custom-args/captures/delayedRunops.scala b/tests/neg-custom-args/captures/delayedRunops.scala index 1108be9c938a..946d4324ddeb 100644 --- a/tests/neg-custom-args/captures/delayedRunops.scala +++ b/tests/neg-custom-args/captures/delayedRunops.scala @@ -20,7 +20,7 @@ import caps.{use, consume} def delayedRunOps2(@consume ops: List[() => Unit]): () ->{} Unit = () => val ops1: List[() => Unit] = ops // error - runOps(ops1) // error + runOps(ops1) // was error // unsound: impure operation pretended pure def delayedRunOps3(ops: List[() => Unit]): () ->{} Unit = diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index e7483e10bfa6..71680300046c 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -23,7 +23,3 @@ | ^^^^^^^^^^^^^^^^^^^^^^^^ |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> (ex$20: caps.Exists) -> Boxed[box IO^{ex$20}] since |the part box IO^{ex$20} of that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/i21401.scala:18:21 ------------------------------------------------------------ -18 | val y: IO^{x*} = x.unbox // error - | ^^^^^^^ - | Local reach capability x* leaks into capture scope of method test2 diff --git a/tests/neg-custom-args/captures/i21401.scala b/tests/neg-custom-args/captures/i21401.scala index 0b5479376a0a..f6071e2a47d5 100644 --- a/tests/neg-custom-args/captures/i21401.scala +++ b/tests/neg-custom-args/captures/i21401.scala @@ -15,5 +15,5 @@ def test2() = val a = usingIO[IO^](x => x) // error val leaked: [R, X <: Boxed[IO^] -> R] -> (op: X) -> R = usingIO[Res](mkRes) // error val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error - val y: IO^{x*} = x.unbox // error + val y: IO^{x*} = x.unbox // was error y.println("boom") diff --git a/tests/neg-custom-args/captures/i21442.check b/tests/neg-custom-args/captures/i21442.check index 1f8fbf4190d9..a7fa0553192e 100644 --- a/tests/neg-custom-args/captures/i21442.check +++ b/tests/neg-custom-args/captures/i21442.check @@ -3,10 +3,6 @@ | ^^^^^^^ | Local reach capability x* leaks into capture scope of method foo. | To allow this, the parameter x should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/i21442.scala:18:14 ------------------------------------------------------------ -18 | val io = x1.unbox // error - | ^^^^^^^^ - | Local reach capability x1* leaks into capture scope of method bar -- Error: tests/neg-custom-args/captures/i21442.scala:17:10 ------------------------------------------------------------ 17 | val x1: Boxed[IO^] = x // error | ^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/i21442.scala b/tests/neg-custom-args/captures/i21442.scala index 3541bd89789a..16d32c5218cb 100644 --- a/tests/neg-custom-args/captures/i21442.scala +++ b/tests/neg-custom-args/captures/i21442.scala @@ -15,5 +15,5 @@ def foo(x: Boxed[IO^]): Unit = // But, no type error reported. def bar(x: Boxed[IO^]): Unit = val x1: Boxed[IO^] = x // error - val io = x1.unbox // error + val io = x1.unbox // was error io.use() diff --git a/tests/neg-custom-args/captures/leak-problem.scala b/tests/neg-custom-args/captures/leak-problem.scala index c842280c0587..9a9534052d30 100644 --- a/tests/neg-custom-args/captures/leak-problem.scala +++ b/tests/neg-custom-args/captures/leak-problem.scala @@ -25,7 +25,7 @@ def test(): Unit = def useBoxedAsync1(@use x: Box[Async^]): Unit = x.get.read() val xs: Box[Async^] = ??? - val xsLambda = () => useBoxedAsync(xs) // error + val xsLambda = () => useBoxedAsync(xs) // was error now ok val _: () ->{xs*} Unit = xsLambda val _: () -> Unit = xsLambda // error diff --git a/tests/neg-custom-args/captures/path-use.check b/tests/neg-custom-args/captures/path-use.check deleted file mode 100644 index e09ee232dd17..000000000000 --- a/tests/neg-custom-args/captures/path-use.check +++ /dev/null @@ -1,4 +0,0 @@ --- Error: tests/neg-custom-args/captures/path-use.scala:18:32 ---------------------------------------------------------- -18 | val g = () => println(c.procs.head) // error, local reach capability c.procs* leaks - | ^^^^^^^^^^^^ - | Local reach capability c.procs* leaks into capture scope of method test diff --git a/tests/neg-custom-args/captures/path-use.scala b/tests/neg-custom-args/captures/path-use.scala deleted file mode 100644 index 31feb4c0adf4..000000000000 --- a/tests/neg-custom-args/captures/path-use.scala +++ /dev/null @@ -1,25 +0,0 @@ -import language.experimental.namedTuples - -class IO - -class C(val f: IO^): - val procs: List[Proc] = ??? - -type Proc = () => Unit - -def test(io: IO^) = - val c = C(io) - val f = () => println(c.f) - val _: () ->{c.f} Unit = f - - val x = c.procs - val _: List[() ->{c.procs*} Unit] = x - - val g = () => println(c.procs.head) // error, local reach capability c.procs* leaks - val _: () ->{c.procs*} Unit = g - - val cc: C { val f: IO^{io}; val procs: List[() ->{io} Unit] }^{io} = - ??? - - val gg = () => println(cc.procs.head) // OK, since cc.procs* has {io} as underlying capture set - val _: () ->{io} Unit = gg diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 008b22d9cd8e..48544cb38829 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -44,10 +44,6 @@ | ^ | Type variable A of constructor Id cannot be instantiated to box () => Unit since | that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/reaches.scala:56:6 ------------------------------------------------------------ -56 | id(() => f.write()) // error - | ^^^^^^^^^^^^^^^^^^^ - | Local reach capability id* leaks into capture scope of method test -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:63:27 -------------------------------------- 63 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error | ^^^^^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index d4f9ceee3de2..9f03e0659d46 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -53,7 +53,7 @@ class Id[-A, +B >: A](): def test = val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error usingFile: f => - id(() => f.write()) // error + id(() => f.write()) // was error def attack2 = val id: File^ -> File^ = x => x diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index c4905cee258c..c02d95904e13 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -10,10 +10,6 @@ | Required: Foo[box File^] | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:25:22 --------------------------------------------------- -25 | escaped = boom.use(f) // error - | ^^^^^^^^^^^ - | Local reach capability backdoor* leaks into capture scope of method bad -- [E164] Declaration Error: tests/neg-custom-args/captures/unsound-reach-4.scala:17:6 --------------------------------- 17 | def use(@consume x: F): File^ = x // error @consume override | ^ diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index d636afd027d4..715dfcc90ef8 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -22,4 +22,4 @@ def bad(): Unit = var escaped: File^{backdoor*} = null withFile("hello.txt"): f => - escaped = boom.use(f) // error + escaped = boom.use(f) // was error diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 17d4a4420833..a1ebd30e4915 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -15,8 +15,3 @@ | 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 - | ^ - | Local reach capability backdoor* leaks into capture scope of method bad -24 | escaped = f1 diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index 0aa7f1fc7ee9..a53b091c5918 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -20,6 +20,6 @@ def bad(): Unit = var escaped: File^{backdoor*} = null withFile("hello.txt"): f => - boom.use(f): (f1: File^{backdoor*}) => // error + boom.use(f): (f1: File^{backdoor*}) => // was error escaped = f1 diff --git a/tests/pending/pos-custom-args/captures/sep-pairs.scala b/tests/pending/pos-custom-args/captures/sep-pairs.scala new file mode 100644 index 000000000000..c96a7547a2b9 --- /dev/null +++ b/tests/pending/pos-custom-args/captures/sep-pairs.scala @@ -0,0 +1,28 @@ +import caps.Mutable +import caps.cap + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +case class Pair[+X, +Y](val fst: X, val snd: Y) + +def twoRefs(): Pair[Ref^, Ref^] = + val r1 = Ref() + val r2 = Ref() + Pair(r1, r2) + +def twoRefsBad(): Pair[Ref^, Ref^] = + Pair(Ref(), Ref()) // error: universal capability cannot be included in capture set + // even though this is morally equivalent to `twoRefs` + + +def test(io: Object^): Unit = + val two = twoRefs() + val fst = two.fst // error: local reach capability two* leaks into test + // first, the leakage makes no sense + // second, the capture should be two.fst, not two* + val snd = two.snd + val three: Pair[Ref^{io}, Ref^{io}] = ??? + val bad = three.fst diff --git a/tests/pos-custom-args/captures/path-use.scala b/tests/pos-custom-args/captures/path-use.scala index 629fa04315a7..e738b81ae970 100644 --- a/tests/pos-custom-args/captures/path-use.scala +++ b/tests/pos-custom-args/captures/path-use.scala @@ -1,5 +1,4 @@ import language.experimental.namedTuples -import caps.use class IO @@ -9,14 +8,18 @@ class C(val f: IO^): type Proc = () => Unit def test(io: IO^) = - def test1(@use c: C { val f: IO^{io}}^{io}) = - val f = () => println(c.f) - val _: () ->{c.f} Unit = f + val c = C(io) + val f = () => println(c.f) + val _: () ->{c.f} Unit = f - val x = c.procs - val _: List[() ->{c.procs*} Unit] = x + val x = c.procs + val _: List[() ->{c.procs*} Unit] = x - val g = () => println(c.procs.head) - val _: () ->{c.procs*} Unit = g - test1(C(io)) + val g = () => println(c.procs.head) // was error, local reach capability c.procs* leaks + val _: () ->{c.procs*} Unit = g + val cc: C { val f: IO^{io}; val procs: List[() ->{io} Unit] }^{io} = + ??? + + val gg = () => println(cc.procs.head) // OK, since cc.procs* has {io} as underlying capture set + val _: () ->{io} Unit = gg From 91eef3202ce6bd401846bc2020e2d53db9a92142 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 4 Feb 2025 22:31:58 +0100 Subject: [PATCH 30/93] Tweak error message in SepCheck --- compiler/src/dotty/tools/dotc/cc/SepCheck.scala | 5 +++-- compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 39f999fa6084..03939e4f0bb4 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -630,10 +630,11 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: .dropWhile(_._3.isEmpty) .nextOption .getOrElse(("", current, globalOverlap)) + val alsoStr = if next == prevRefs && nextRel == prevRel then "also " else "" report.error( em"""Separation failure in ${role.description} $tpe. |One part, $part , $nextRel ${CaptureSet(next)}. - |A previous part$prevStr $prevRel ${CaptureSet(prevRefs)}. + |A previous part$prevStr $alsoStr$prevRel ${CaptureSet(prevRefs)}. |The two sets overlap at ${CaptureSet(overlap)}.""", pos) @@ -642,7 +643,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val partHidden = prune(partRefs.hidden.footprint, tpe, role).deduct(partFootprint) checkSep(footprint, partHidden, identity, "references", "hides") - checkSep(hiddenSet, partHidden, _.hidden, "also hides", "hides") + checkSep(hiddenSet, partHidden, _.hidden, "hides", "hides") checkSep(hiddenSet, partFootprint, _.hidden, "hides", "references") footprint ++= partFootprint diff --git a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala index 3adb3ab0ce7d..e6fe64fe7b62 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala @@ -18,7 +18,6 @@ import dotty.tools.dotc.core.Types.NoType import dotty.tools.dotc.core.Types.Type import dotty.tools.dotc.core.Types import dotty.tools.dotc.semanticdb.TypeOps -import dotty.tools.dotc.cc.boxedCaptureSet import dotty.tools.dotc.core.Symbols.{NoSymbol, isParamOrAccessor} import scala.collection.mutable import dotty.tools.dotc.core.Scopes.Scope From cf48a6c57b124a97ba6577c241e5eceec6c03efc Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 4 Feb 2025 22:33:53 +0100 Subject: [PATCH 31/93] Add reach capabilities on the fly in getBoxed --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 7692cf191bf9..67ffeae8050e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -320,18 +320,27 @@ extension (tp: Type) /** The capture set consisting of all top-level captures of `tp` that appear under a box. * Unlike for `boxed` this also considers parents of capture types, unions and * intersections, and type proxies other than abstract types. + * Furthermore, if the original type is a capture ref `x`, it replaces boxed universal sets + * on the fly with x*. */ def boxedCaptureSet(using Context): CaptureSet = - def getBoxed(tp: Type): CaptureSet = tp match + def getBoxed(tp: Type, pre: Type): CaptureSet = tp match case tp @ CapturingType(parent, refs) => - val pcs = getBoxed(parent) - if tp.isBoxed then refs ++ pcs else pcs + val pcs = getBoxed(parent, pre) + if !tp.isBoxed then + pcs + else if pre.exists && refs.containsRootCapability then + val reachRef = if refs.isReadOnly then pre.reach.readOnly else pre.reach + pcs ++ reachRef.singletonCaptureSet + else + pcs ++ refs + case ref: CaptureRef if ref.isTracked && !pre.exists => getBoxed(ref, ref) case tp: TypeRef if tp.symbol.isAbstractOrParamType => CaptureSet.empty - case tp: TypeProxy => getBoxed(tp.superType) - case tp: AndType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) - case tp: OrType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) + case tp: TypeProxy => getBoxed(tp.superType, pre) + case tp: AndType => getBoxed(tp.tp1, pre) ** getBoxed(tp.tp2, pre) + case tp: OrType => getBoxed(tp.tp1, pre) ++ getBoxed(tp.tp2, pre) case _ => CaptureSet.empty - getBoxed(tp) + getBoxed(tp, NoType) /** Is the boxedCaptureSet of this type nonempty? */ def isBoxedCapturing(using Context): Boolean = From b4ff2fa9c9566bd73a23be3eb17a443b8b26c0a2 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 4 Feb 2025 23:53:19 +0100 Subject: [PATCH 32/93] Revise path handling - Don't widen prefix type if followed by a path selection - When applying VAR with a prefix path p on a boxed capture set, replace cap with p* instead of p. This makes it clear that we need to charge deep capture sets on parameters and avoids paradoxes like this one: case class Box[+T](get: T) val b: Box[C^] Here, b.get <:< b, but b.get's underlying capture set is {cap} whereas b's underlying capture set is {}. By using b.get* instead of b.get, we make sure that we compare against the dcs of `b`, which is also {cap}. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +++ .../dotty/tools/dotc/cc/CheckCaptures.scala | 29 +++++++++++++++---- .../src/scala/collection/View.scala | 10 +++++-- tests/neg-custom-args/captures/i21442.check | 2 +- tests/pos-custom-args/captures/i15749.scala | 5 ++-- .../pos-custom-args/captures/sep-pairs.scala | 11 ++++--- .../colltest5/CollectionStrawManCC5_1.scala | 8 +++-- 7 files changed, 48 insertions(+), 21 deletions(-) rename tests/{pending => }/pos-custom-args/captures/sep-pairs.scala (66%) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 67ffeae8050e..3d84a37d703b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -56,6 +56,10 @@ object ccConfig: def useSepChecks(using Context): Boolean = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) + /** Not used currently. Handy for trying out new features */ + def newScheme(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) + end ccConfig /** Are we at checkCaptures phase? */ diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 1513d4422e2f..c7d88ba3adf4 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -643,7 +643,7 @@ class CheckCaptures extends Recheck, SymTransformer: // - on the LHS of assignments, or // - if the qualifier or selection type is boxed, or // - the selection is either a trackable capture ref or a pure type - if pt == LhsProto + if noWiden(selType, pt) || qualType.isBoxedCapturing || selWiden.isBoxedCapturing || selType.isTrackableRef @@ -1496,11 +1496,14 @@ class CheckCaptures extends Recheck, SymTransformer: * Then * foo: Foo { def a: C^{foo}; def b: C^{foo} }^{foo} */ - private def improveCaptures(widened: Type, actual: Type)(using Context): Type = actual match + private def improveCaptures(widened: Type, prefix: Type)(using Context): Type = prefix match case ref: CaptureRef if ref.isTracked => widened match - case CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) => - widened.derivedCapturingType(p, ref.singletonCaptureSet) + case widened @ CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) => + val improvedCs = + if widened.isBoxed then ref.reach.singletonCaptureSet + else ref.singletonCaptureSet + widened.derivedCapturingType(p, improvedCs) .showing(i"improve $widened to $result", capt) case _ => widened case _ => widened @@ -1524,13 +1527,29 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => actual + /* Currently not needed since it forms part of `adapt` + private def improve(actual: Type, prefix: Type)(using Context): Type = + val widened = actual.widen.dealiasKeepAnnots + val improved = improveCaptures(widened, prefix).withReachCaptures(prefix) + if improved eq widened then actual else improved + */ + + /** An actual singleton type should not be widened if the expected type is a + * LhsProto, or a singleton type, or a path selection with a stable value + */ + private def noWiden(actual: Type, expected: Type)(using Context): Boolean = + actual.isSingleton + && expected.match + case expected: PathSelectionProto => !expected.sym.isOneOf(UnstableValueFlags) + case _ => expected.isSingleton || expected == LhsProto + /** Adapt `actual` type to `expected` type. This involves: * - narrow toplevel captures of `x`'s underlying type to `{x}` according to CC's VAR rule * - narrow nested captures of `x`'s underlying type to `{x*}` * - do box adaptation */ def adapt(actual: Type, expected: Type, tree: Tree, boxErrors: BoxErrors)(using Context): Type = - if expected == LhsProto || expected.isSingleton && actual.isSingleton then + if noWiden(actual, expected) then actual else val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) diff --git a/scala2-library-cc/src/scala/collection/View.scala b/scala2-library-cc/src/scala/collection/View.scala index 482884835cb1..72a073836e77 100644 --- a/scala2-library-cc/src/scala/collection/View.scala +++ b/scala2-library-cc/src/scala/collection/View.scala @@ -151,12 +151,16 @@ object View extends IterableFactory[View] { object Filter { def apply[A](underlying: Iterable[A]^, p: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, p} = underlying match { - case filter: Filter[A]^{underlying} if filter.isFlipped == isFlipped => - unsafeAssumeSeparate: + case filter: Filter[A] if filter.isFlipped == isFlipped => + new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) + .asInstanceOf[Filter[A]^{underlying, p}] + // !!! asInstanceOf needed once paths were added, see path-patmat-should-be-pos.scala for minimization + //case filter: Filter[A]^{underlying} if filter.isFlipped == isFlipped => + // unsafeAssumeSeparate: // See filter-iterable.scala for a test where a variant of Filter // works without the unsafeAssumeSeparate. But it requires significant // changes compared to the version here. See also Filter in colltest5.CollectionStrawManCC5_1. - new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) + // new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) case _ => new Filter(underlying, p, isFlipped) } } diff --git a/tests/neg-custom-args/captures/i21442.check b/tests/neg-custom-args/captures/i21442.check index a7fa0553192e..66bc0727f412 100644 --- a/tests/neg-custom-args/captures/i21442.check +++ b/tests/neg-custom-args/captures/i21442.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/i21442.scala:10:13 ------------------------------------------------------------ 10 | val io = x.unbox // error: local reach capability {x*} leaks | ^^^^^^^ - | Local reach capability x* leaks into capture scope of method foo. + | Local reach capability x.unbox* leaks into capture scope of method foo. | To allow this, the parameter x should be declared with a @use annotation -- Error: tests/neg-custom-args/captures/i21442.scala:17:10 ------------------------------------------------------------ 17 | val x1: Boxed[IO^] = x // error diff --git a/tests/pos-custom-args/captures/i15749.scala b/tests/pos-custom-args/captures/i15749.scala index 58274c7cc817..dd92aced77a5 100644 --- a/tests/pos-custom-args/captures/i15749.scala +++ b/tests/pos-custom-args/captures/i15749.scala @@ -1,5 +1,4 @@ -//> using options -source 3.4 -// (to make sure we use the sealed policy) +import caps.use class Unit object unit extends Unit @@ -12,6 +11,6 @@ class Foo[T](val x: T) // Foo[□ Unit => T] type BoxedLazyVal[T] = Foo[LazyVal[T]] -def force[A](v: BoxedLazyVal[A]): A = +def force[A](@use v: BoxedLazyVal[A]): A = // Γ ⊢ v.x : □ {cap} Unit -> A v.x(unit) // should be error: (unbox v.x)(unit), where (unbox v.x) should be untypable, now ok \ No newline at end of file diff --git a/tests/pending/pos-custom-args/captures/sep-pairs.scala b/tests/pos-custom-args/captures/sep-pairs.scala similarity index 66% rename from tests/pending/pos-custom-args/captures/sep-pairs.scala rename to tests/pos-custom-args/captures/sep-pairs.scala index c96a7547a2b9..e1441ac2e1e2 100644 --- a/tests/pending/pos-custom-args/captures/sep-pairs.scala +++ b/tests/pos-custom-args/captures/sep-pairs.scala @@ -13,16 +13,15 @@ def twoRefs(): Pair[Ref^, Ref^] = val r2 = Ref() Pair(r1, r2) +/* def twoRefsBad(): Pair[Ref^, Ref^] = Pair(Ref(), Ref()) // error: universal capability cannot be included in capture set // even though this is morally equivalent to `twoRefs` - +*/ def test(io: Object^): Unit = val two = twoRefs() - val fst = two.fst // error: local reach capability two* leaks into test - // first, the leakage makes no sense - // second, the capture should be two.fst, not two* + val fst = two.fst val snd = two.snd - val three: Pair[Ref^{io}, Ref^{io}] = ??? - val bad = three.fst + + val p: Pair[Ref^, Ref^] = Pair(fst, snd) diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index 54e5f7e2c6fd..0d09e5c8cb40 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -455,12 +455,14 @@ object CollectionStrawMan5 { object Filter: def apply[A](underlying: Iterable[A]^, pp: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, pp} = underlying match - case filter: Filter[A]^{underlying} => - unsafeAssumeSeparate: + case filter: Filter[A] => + new Filter(filter.underlying, a => filter.p(a) && pp(a)) + .asInstanceOf[Filter[A]^{underlying, pp}] + //unsafeAssumeSeparate: // See filter-iterable.scala for a test where a variant of Filter // works without the unsafeAssumeSeparate. But it requires significant // changes compared to the version here. - new Filter(filter.underlying, a => filter.p(a) && pp(a)) + //new Filter(filter.underlying, a => filter.p(a) && pp(a)) case _ => new Filter(underlying, pp) case class Partition[A](val underlying: Iterable[A]^, p: A => Boolean) { From e2f44e4e5e4ae30e306cd17fc8675442b6e0c207 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 6 Feb 2025 11:34:35 +0100 Subject: [PATCH 33/93] Don't avoid elements of hidden sets It fixes one of the problems in sep-pairs. It probably won't matter anymore for correctness once we revise overlap calculations, but it will give better error messages. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 3 ++ .../neg-custom-args/captures/sep-pairs.scala | 45 +++++++++++++++++++ .../pos-custom-args/captures/sep-pairs.scala | 27 ----------- 3 files changed, 48 insertions(+), 27 deletions(-) create mode 100644 tests/neg-custom-args/captures/sep-pairs.scala delete mode 100644 tests/pos-custom-args/captures/sep-pairs.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 55fda0f22a08..551e687854e2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -17,6 +17,7 @@ import typer.ErrorReporting.Addenda import util.common.alwaysTrue import scala.collection.{mutable, immutable} import CCState.* +import TypeOps.AvoidMap /** A class for capture sets. Capture sets can be constants or variables. * Capture sets support inclusion constraints <:< where <:< is subcapturing. @@ -329,6 +330,8 @@ sealed abstract class CaptureSet extends Showable: else BiMapped(asVar, tm, mappedElems) case tm: IdentityCaptRefMap => this + case tm: AvoidMap if this.isInstanceOf[HiddenSet] => + this case _ => val mapped = mapRefs(elems, tm, tm.variance) if isConst then diff --git a/tests/neg-custom-args/captures/sep-pairs.scala b/tests/neg-custom-args/captures/sep-pairs.scala new file mode 100644 index 000000000000..4b25e6d61719 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-pairs.scala @@ -0,0 +1,45 @@ +import caps.Mutable +import caps.cap + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +class Pair[+X, +Y](val fst: X, val snd: Y) + +def mkPair[X](x: X): Pair[X, X] = Pair(x, x) + +def bad: Pair[Ref^, Ref^] = // error: overlap at r1*, r0 + val r0 = Ref() + val r1: Pair[Ref^, Ref^] = mkPair(r0) // error: overlap at r0 + r1 + +class SamePair[+X](val fst: X, val snd: X) + +def twoRefs(): Pair[Ref^, Ref^] = + val r1 = Ref() + val r2 = Ref() + Pair(r1, r2) + +def twoRefs2(): SamePair[Ref^] = + val r1 = Ref() + val r2 = Ref() + val r3: SamePair[Ref^] = SamePair(r1, r1) // ok + r3 + +def twoRefsBad(): Pair[Ref^, Ref^] = + Pair(Ref(), Ref()) // error // error: universal capability cannot be included in capture set + // but should work since this is morally equivalent to `twoRefs` + +def test(io: Object^): Unit = + val two = twoRefs() + val fst: Ref^{two.fst*} = two.fst + val snd: Ref^{two.snd*} = two.snd + + val two2 = twoRefs2() + val fst2 = two.fst + val snd2 = two.snd + + val p2: Pair[Ref^, Ref^] = Pair(fst, snd) // should be error + diff --git a/tests/pos-custom-args/captures/sep-pairs.scala b/tests/pos-custom-args/captures/sep-pairs.scala deleted file mode 100644 index e1441ac2e1e2..000000000000 --- a/tests/pos-custom-args/captures/sep-pairs.scala +++ /dev/null @@ -1,27 +0,0 @@ -import caps.Mutable -import caps.cap - -class Ref extends Mutable: - var x = 0 - def get: Int = x - mut def put(y: Int): Unit = x = y - -case class Pair[+X, +Y](val fst: X, val snd: Y) - -def twoRefs(): Pair[Ref^, Ref^] = - val r1 = Ref() - val r2 = Ref() - Pair(r1, r2) - -/* -def twoRefsBad(): Pair[Ref^, Ref^] = - Pair(Ref(), Ref()) // error: universal capability cannot be included in capture set - // even though this is morally equivalent to `twoRefs` -*/ - -def test(io: Object^): Unit = - val two = twoRefs() - val fst = two.fst - val snd = two.snd - - val p: Pair[Ref^, Ref^] = Pair(fst, snd) From 4d04d35f532b7d989abe83d04f87f57aa9e74306 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Feb 2025 10:35:56 +0100 Subject: [PATCH 34/93] Shorten transitive hidden sets and replace cycles by aliases --- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 3 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 85 +++++++++++++++++-- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 2 +- .../tools/dotc/util/SimpleIdentitySet.scala | 22 +++-- 4 files changed, 95 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index a2ceb1f20372..6c4d8880636e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -253,7 +253,8 @@ trait CaptureRef extends TypeProxy, ValueType: * fail a comparison. */ def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = - this.match + (this eq y) + || this.match case Fresh.Cap(hidden) => vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) || !y.stripReadOnly.isCap && canAddHidden && vs.addHidden(hidden, y) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 551e687854e2..1e5a89e86e87 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -168,7 +168,7 @@ 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 = + protected def addIfHiddenOrFail(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = if elems.exists(_.maxSubsumes(elem, canAddHidden = true)) then CompareResult.OK else CompareResult.Fail(this :: Nil) @@ -438,7 +438,7 @@ object CaptureSet: def isAlwaysEmpty = elems.isEmpty def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - addHiddenElem(elem) + addIfHiddenOrFail(elem) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -487,7 +487,10 @@ object CaptureSet: private var isSolved: Boolean = false /** The elements currently known to be in the set */ - var elems: Refs = initialElems + protected var myElems: Refs = initialElems + + def elems: Refs = myElems + def elems_=(refs: Refs): Unit = myElems = refs /** The sets currently known to be dependent sets (i.e. new additions to this set * are propagated to these dependent sets.) @@ -535,7 +538,7 @@ object CaptureSet: final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen - addHiddenElem(elem) + addIfHiddenOrFail(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 @@ -925,10 +928,76 @@ 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 */ + /** A capture set variable used to record the references hidden by a Fresh.Cap instance, + * The elems and deps members are repurposed as follows: + * elems: Set of hidden references + * deps : Set of hidden sets for which the Fresh.Cap instance owning this set + * is a hidden element. + * Hidden sets may become aliases of other hidden sets, which means that + * reads and writes of elems go to the alias. + * If H is an alias of R.hidden for some Fresh.Cap R then: + * H.elems == {R} + * H.deps = {R.hidden} + * This encoding was chosen because it relies only on the elems and deps fields + * which are already subject through snapshotting and rollbacks in VarState. + * It's advantageous if we don't need to deal with other pieces of state there. + */ class HiddenSet(initialHidden: Refs = emptyRefs)(using @constructorOnly ictx: Context) extends Var(initialElems = initialHidden): + private def aliasRef: AnnotatedType | Null = + if myElems.size == 1 then + myElems.nth(0) match + case al @ Fresh.Cap(hidden) if deps.contains(hidden) => al + case _ => null + else null + + private def aliasSet: HiddenSet = + if myElems.size == 1 then + myElems.nth(0) match + case Fresh.Cap(hidden) if deps.contains(hidden) => hidden + case _ => this + else this + + override def elems: Refs = + val al = aliasSet + if al eq this then super.elems else al.elems + + override def elems_=(refs: Refs) = + val al = aliasSet + if al eq this then super.elems_=(refs) else al.elems_=(refs) + + /** Add element to hidden set. Also add it to all supersets (as indicated by + * deps of this set). Follow aliases on both hidden set and added element + * before adding. If the added element is also a Fresh.Cap instance with + * hidden set H which is a superset of this set, then make this set an + * alias of H. + */ + def add(elem: CaptureRef)(using ctx: Context, vs: VarState): Unit = + val alias = aliasSet + if alias ne this then alias.add(elem) + else + def addToElems() = + elems += elem + deps.foreach: dep => + assert(dep != this) + vs.addHidden(dep.asInstanceOf[HiddenSet], elem) + elem match + case Fresh.Cap(hidden) => + if this ne hidden then + val alias = hidden.aliasRef + if alias != null then + add(alias) + else if deps.contains(hidden) then // make this an alias of elem + capt.println(i"Alias $this to $hidden") + elems = SimpleIdentitySet(elem) + deps = SimpleIdentitySet(hidden) + else + addToElems() + hidden.deps += this + case _ => + addToElems() + /** Apply function `f` to `elems` while setting `elems` to empty for the * duration. This is used to escape infinite recursions if two Fresh.Caps * refer to each other in their hidden sets. @@ -1075,9 +1144,11 @@ object CaptureSet: */ def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = elemsMap.get(hidden) match - case None => elemsMap(hidden) = hidden.elems + case None => + elemsMap(hidden) = hidden.elems + depsMap(hidden) = hidden.deps case _ => - hidden.elems += elem + hidden.add(elem)(using ctx, this) true /** Roll back global state to what was recorded in this VarState */ diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 48b20f18f027..6ec914f3e7a5 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -62,7 +62,7 @@ object Fresh: def apply(owner: Symbol)(using Context): CaptureRef = apply(ownerToHidden(owner, reach = false)) - def unapply(tp: AnnotatedType)(using Context): Option[CaptureSet.HiddenSet] = tp.annot match + def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match case Annot(hidden) => Some(hidden) case _ => None end Cap diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index 03392fe8cb23..a807452e5260 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -20,10 +20,12 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { acc def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A def toList: List[Elem] - def iterator: Iterator[Elem] + def nth(n: Int): Elem final def isEmpty: Boolean = size == 0 + final def iterator: Iterator[Elem] = Iterator.tabulate(size)(nth) + def forall[E >: Elem <: AnyRef](p: E => Boolean): Boolean = !exists(!p(_)) def filter(p: Elem => Boolean): SimpleIdentitySet[Elem] = @@ -74,7 +76,7 @@ object SimpleIdentitySet { override def map[B <: AnyRef](f: Nothing => B): SimpleIdentitySet[B] = empty def /: [A, E <: AnyRef](z: A)(f: (A, E) => A): A = z def toList = Nil - def iterator = Iterator.empty + def nth(n: Int): Nothing = throw new IndexOutOfBoundsException(n.toString) } private class Set1[+Elem <: AnyRef](x0: AnyRef) extends SimpleIdentitySet[Elem] { @@ -92,7 +94,9 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(z, x0.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: Nil - def iterator = Iterator.single(x0.asInstanceOf[Elem]) + def nth(n: Int) = + if n == 0 then x0.asInstanceOf[Elem] + else throw new IndexOutOfBoundsException(n.toString) } private class Set2[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef) extends SimpleIdentitySet[Elem] { @@ -114,10 +118,10 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: Nil - def iterator = Iterator.tabulate(2) { + def nth(n: Int) = n match case 0 => x0.asInstanceOf[Elem] case 1 => x1.asInstanceOf[Elem] - } + case _ => throw new IndexOutOfBoundsException(n.toString) } private class Set3[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef, x2: AnyRef) extends SimpleIdentitySet[Elem] { @@ -154,11 +158,11 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]), x2.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: x2.asInstanceOf[Elem] :: Nil - def iterator = Iterator.tabulate(3) { + def nth(n: Int) = n match case 0 => x0.asInstanceOf[Elem] case 1 => x1.asInstanceOf[Elem] case 2 => x2.asInstanceOf[Elem] - } + case _ => throw new IndexOutOfBoundsException(n.toString) } private class SetN[+Elem <: AnyRef](val xs: Array[AnyRef]) extends SimpleIdentitySet[Elem] { @@ -205,7 +209,9 @@ object SimpleIdentitySet { foreach(buf += _) buf.toList } - def iterator = xs.iterator.asInstanceOf[Iterator[Elem]] + def nth(n: Int) = + if 0 <= n && n < size then xs(n).asInstanceOf[Elem] + else throw new IndexOutOfBoundsException(n.toString) override def ++ [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): SimpleIdentitySet[E] = that match { case that: SetN[?] => From f7611839d1d82a29652b9d1e563f030e651db1f3 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 11 Feb 2025 13:22:53 +0100 Subject: [PATCH 35/93] Use peaks-based checking for applications --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 9 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../src/dotty/tools/dotc/cc/Existential.scala | 6 +- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 14 +- .../src/dotty/tools/dotc/cc/SepCheck.scala | 212 ++++++++++++------ .../tools/dotc/printing/PlainPrinter.scala | 3 +- .../tools/dotc/util/SimpleIdentitySet.scala | 4 + .../captures/filevar-expanded.check | 29 +-- tests/neg-custom-args/captures/lazyref.check | 11 +- tests/neg-custom-args/captures/reaches.check | 50 ++--- tests/neg-custom-args/captures/reaches2.check | 25 +-- .../captures/sep-compose.check | 168 +++++++------- tests/neg-custom-args/captures/sep-use2.scala | 2 +- .../neg-custom-args/captures/sepchecks2.check | 25 +-- .../neg-custom-args/captures/sepchecks4.check | 25 +-- 15 files changed, 324 insertions(+), 261 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 1e5a89e86e87..0fe1b762cb56 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -18,6 +18,7 @@ import util.common.alwaysTrue import scala.collection.{mutable, immutable} import CCState.* import TypeOps.AvoidMap +import compiletime.uninitialized /** A class for capture sets. Capture sets can be constants or variables. * Capture sets support inclusion constraints <:< where <:< is subcapturing. @@ -942,8 +943,9 @@ object CaptureSet: * which are already subject through snapshotting and rollbacks in VarState. * It's advantageous if we don't need to deal with other pieces of state there. */ - class HiddenSet(initialHidden: Refs = emptyRefs)(using @constructorOnly ictx: Context) - extends Var(initialElems = initialHidden): + class HiddenSet(owner: Symbol, initialHidden: Refs = emptyRefs)(using @constructorOnly ictx: Context) + extends Var(owner, initialHidden): + var owningCap: AnnotatedType = uninitialized private def aliasRef: AnnotatedType | Null = if myElems.size == 1 then @@ -959,6 +961,9 @@ object CaptureSet: case _ => this else this + def superCaps: List[AnnotatedType] = + deps.toList.map(_.asInstanceOf[HiddenSet].owningCap) + override def elems: Refs = val al = aliasSet if al eq this then super.elems else al.elems diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c7d88ba3adf4..49ad33ad53da 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -803,7 +803,7 @@ class CheckCaptures extends Recheck, SymTransformer: var refined: Type = core var allCaptures: CaptureSet = if core.derivesFromMutable then initCs ++ CaptureSet.fresh() - else if core.derivesFromCapability then initCs ++ Fresh.Cap().readOnly.singletonCaptureSet + else if core.derivesFromCapability then initCs ++ Fresh.Cap(core.classSymbol).readOnly.singletonCaptureSet else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index f115adfa6421..9aa8793e0889 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -245,7 +245,7 @@ object Existential: /** Map top-level existentials to `Fresh.Cap`. */ def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - unpacked.substParam(boundVar, Fresh.Cap()) + unpacked.substParam(boundVar, Fresh.Cap(NoSymbol)) 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, Fresh.Cap())) + toCapDeeply(unpacked.substParam(boundVar, Fresh.Cap(NoSymbol))) case tp1 @ FunctionOrMethod(args, res) => val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) if tp2 ne tp1 then tp2 else tp @@ -317,7 +317,7 @@ object Existential: //.showing(i"mapcap $t = $result") lazy val inverse = new BiTypeMap: - lazy val freshCap = Fresh.Cap() + lazy val freshCap = Fresh.Cap(NoSymbol) def apply(t: Type) = t match case t: TermParamRef if t eq boundVar => freshCap case _ => mapOver(t) diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 6ec914f3e7a5..d6693498e4b7 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -50,17 +50,21 @@ object Fresh: /** An extractor for "fresh" capabilities */ object Cap: - def apply(initialHidden: Refs = emptyRefs)(using Context): CaptureRef = + def apply(owner: Symbol, initialHidden: Refs = emptyRefs)(using Context): CaptureRef = if ccConfig.useSepChecks then - AnnotatedType(defn.captureRoot.termRef, Annot(CaptureSet.HiddenSet(initialHidden))) + val hiddenSet = CaptureSet.HiddenSet(owner, initialHidden) + val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet)) + hiddenSet.owningCap = res + //assert(hiddenSet.id != 3) + res else defn.captureRoot.termRef def apply(owner: Symbol, reach: Boolean)(using Context): CaptureRef = - apply(ownerToHidden(owner, reach)) + apply(owner, ownerToHidden(owner, reach)) def apply(owner: Symbol)(using Context): CaptureRef = - apply(ownerToHidden(owner, reach = false)) + apply(owner, ownerToHidden(owner, reach = false)) def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match case Annot(hidden) => Some(hidden) @@ -77,7 +81,7 @@ object Fresh: if variance <= 0 then t else t match case t: CaptureRef if t.isCap => - Cap(ownerToHidden(owner, reach)) + Cap(owner, ownerToHidden(owner, reach)) case t @ CapturingType(_, refs) => val savedReach = reach if t.isBoxed then reach = true diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 03939e4f0bb4..bdd9d2723e8e 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -142,6 +142,8 @@ object SepCheck: val EmptyConsumedSet = ConstConsumedSet(Array(), Array()) + case class PeaksPair(actual: Refs, formal: Refs) + class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import checker.* import SepCheck.* @@ -194,6 +196,52 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val elems: Refs = refs.filter(!_.isMaxCapability) recur(elems, elems.toList) + /** The members of type Fresh.Cap(...) or Fresh.Cap(...).rd in the transitive closure + * of this set + */ + private def freshElems(using Context): Refs = + def recur(seen: Refs, acc: Refs, newElems: List[CaptureRef]): Refs = newElems match + case newElem :: newElems1 => + if seen.contains(newElem) then + recur(seen, acc, newElems1) + else newElem.stripReadOnly match + case Fresh.Cap(_) => + recur(seen, acc + newElem, newElems1) + //case _: TypeRef | _: TypeParamRef => + // recur(seen + newElem, acc, newElems1) + case _ => + recur(seen + newElem, acc, newElem.captureSetOfInfo.elems.toList ++ newElems1) + case Nil => acc + recur(emptyRefs, emptyRefs, refs.toList) + + private def peaks(using Context): Refs = + def recur(seen: Refs, acc: Refs, newElems: List[CaptureRef]): Refs = newElems match + case newElem :: newElems1 => + if seen.contains(newElem) then + recur(seen, acc, newElems1) + else newElem.stripReadOnly match + case Fresh.Cap(hidden) => + if hidden.deps.isEmpty then recur(seen + newElem, acc + newElem, newElems1) + else + val superCaps = + if newElem.isReadOnly then hidden.superCaps.map(_.readOnly) + else hidden.superCaps + recur(seen + newElem, acc, superCaps ++ newElems) + case _ => + if newElem.isMaxCapability + //|| newElem.isInstanceOf[TypeRef | TypeParamRef] + then recur(seen + newElem, acc, newElems1) + else recur(seen + newElem, acc, newElem.captureSetOfInfo.elems.toList ++ newElems1) + case Nil => acc + recur(emptyRefs, emptyRefs, refs.toList) + + /** The shared peaks between `refs` and `other` */ + private def sharedWith(other: Refs)(using Context): Refs = + def common(refs1: Refs, refs2: Refs) = + refs1.filter: ref => + !ref.isReadOnly && refs2.exists(_.stripReadOnly eq ref) + common(refs, other) ++ common(other, refs) + /** The overlap of two footprint sets F1 and F2. This contains all exclusive references `r` * such that one of the following is true: * 1. @@ -266,6 +314,11 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: recur(refs) end containsHidden + def hiddenSet(using Context): Refs = + freshElems.flatMap: + case Fresh.Cap(hidden) => hidden.elems + case ReadOnlyCapability(Fresh.Cap(hidden)) => hidden.elems.map(_.readOnly) + /** Subtract all elements that are covered by some element in `others` from this set. */ private def deduct(others: Refs)(using Context): Refs = refs.filter: ref => @@ -297,29 +350,21 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: /** Report a separation failure in an application `fn(args)` * @param fn the function - * @param args the flattened argument lists - * @param argIdx the index of the failing argument in `args`, starting at 0 - * @param overlap the overlap causing the failure - * @param hiddenInArg the hidxden set of the type of the failing argument - * @param footprints a sequence of partial footprints, and the index of the - * last argument they cover. - * @param deps cross argument dependencies: maps argument trees to - * those other arguments that where mentioned by coorresponding - * formal parameters. + * @param parts the function prefix followed by the flattened argument list + * @param polyArg the clashing argument to a polymorphic formal + * @param clashing the argument with which it clashes */ - private def sepApplyError(fn: Tree, args: List[Tree], argIdx: Int, - overlap: Refs, hiddenInArg: Refs, footprints: List[(Refs, Int)], - deps: collection.Map[Tree, List[Tree]])(using Context): Unit = - val arg = args(argIdx) + def sepApplyError(fn: Tree, parts: List[Tree], polyArg: Tree, clashing: Tree)(using Context): Unit = + val polyArgIdx = parts.indexOf(polyArg).ensuring(_ >= 0) - 1 + val clashIdx = parts.indexOf(clashing).ensuring(_ >= 0) def paramName(mt: Type, idx: Int): Option[Name] = mt match case mt @ MethodType(pnames) => if idx < pnames.length then Some(pnames(idx)) else paramName(mt.resType, idx - pnames.length) case mt: PolyType => paramName(mt.resType, idx) case _ => None - def formalName = paramName(fn.nuType.widen, argIdx) match + def formalName = paramName(fn.nuType.widen, polyArgIdx) match case Some(pname) => i"$pname " case _ => "" - def whatStr = if overlap.size == 1 then "this capability is" else "these capabilities are" def qualifier = methPart(fn) match case Select(qual, _) => qual case _ => EmptyTree @@ -329,43 +374,45 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def funStr = if isShowableMethod then i"${fn.symbol}: ${fn.symbol.info}" else i"a function of type ${funType.widen}" - val clashIdx = footprints - .collect: - case (fp, idx) if !hiddenInArg.overlapWith(fp).isEmpty => idx - .head - def whereStr = clashIdx match + def clashArgStr = clashIdx match case 0 => "function prefix" case 1 => "first argument " case 2 => "second argument" case 3 => "third argument " case n => s"${n}th argument " - def clashTree = - if clashIdx == 0 then qualifier - else args(clashIdx - 1) def clashTypeStr = if clashIdx == 0 && !isShowableMethod then "" // we already mentioned the type in `funStr` - else i" with type ${clashTree.nuType}" - def clashCaptures = captures(clashTree) - def hiddenCaptures = formalCaptures(arg).hidden - def clashFootprint = clashCaptures.footprint - def hiddenFootprint = hiddenCaptures.footprint - def declaredFootprint = deps(arg).map(captures(_)).foldLeft(emptyRefs)(_ ++ _).footprint - def footprintOverlap = hiddenFootprint.overlapWith(clashFootprint).deduct(declaredFootprint) + else i" with type ${clashing.nuType}" + val hiddenSet = formalCaptures(polyArg).hiddenSet + val clashSet = captures(clashing) + val hiddenFootprint = hiddenSet.footprint + val clashFootprint = clashSet.footprint + val overlapStr = + // The overlap of footprints, or, of this empty the set of shared peaks. + // We prefer footprint overlap since it tends to be more informative. + val overlap = hiddenFootprint.overlapWith(clashFootprint) + if !overlap.isEmpty then i"${CaptureSet(overlap)}" + else + val sharedPeaks = hiddenSet.peaks.sharedWith(clashSet.peaks) + assert(!sharedPeaks.isEmpty, + i"no overlap for $polyArg: $hiddenSet} vs $clashing: $clashSet") + sharedPeaks.nth(0) match + case fresh @ Fresh.Cap(hidden) => + if hidden.owner.exists then i"cap of ${hidden.owner}" else i"$fresh" + report.error( - em"""Separation failure: argument of type ${arg.nuType} + em"""Separation failure: argument of type ${polyArg.nuType} |to $funStr - |corresponds to capture-polymorphic formal parameter ${formalName}of type ${arg.formalType} - |and captures ${CaptureSet(overlap)}, but $whatStr also passed separately - |in the ${whereStr.trim}$clashTypeStr. + |corresponds to capture-polymorphic formal parameter ${formalName}of type ${polyArg.formalType} + |and hides capabilities ${CaptureSet(hiddenSet)}. + |Some of these overlap with the captures of the ${clashArgStr.trim}$clashTypeStr. | - | Capture set of $whereStr : ${CaptureSet(clashCaptures)} - | Hidden set of current argument : ${CaptureSet(hiddenCaptures)} - | Footprint of $whereStr : ${CaptureSet(clashFootprint)} - | Hidden footprint of current argument : ${CaptureSet(hiddenFootprint)} - | Declared footprint of current argument: ${CaptureSet(declaredFootprint)} - | Undeclared overlap of footprints : ${CaptureSet(footprintOverlap)}""", - arg.srcPos) - end sepApplyError + | Hidden set of current argument : ${CaptureSet(hiddenSet)} + | Hidden footprint of current argument : ${CaptureSet(hiddenSet.footprint)} + | Capture set of $clashArgStr : ${CaptureSet(clashSet)} + | Footprint set of $clashArgStr : ${CaptureSet(clashSet.footprint)} + | The two sets overlap at : $overlapStr""", + polyArg.srcPos) /** Report a use/definition failure, where a previously hidden capability is * used again. @@ -445,37 +492,58 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * formal parameters. */ private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = - val fnCaptures = methPart(fn) match - case Select(qual, _) => qual.nuType.captureSet - case _ => CaptureSet.empty - capt.println(i"check separate $fn($args), fnCaptures = $fnCaptures, argCaptures = ${args.map(arg => CaptureSet(formalCaptures(arg)))}, deps = ${deps.toList}") - var footprint = fnCaptures.elems.footprint - val footprints = mutable.ListBuffer[(Refs, Int)]((footprint, 0)) - val indexedArgs = args.zipWithIndex - - // First, compute all footprints of arguments to monomorphic pararameters, - // separately in `footprints`, and their union in `footprint`. - for (arg, idx) <- indexedArgs do - if !arg.needsSepCheck then - footprint = footprint ++ captures(arg).footprint.deductCapturesOf(deps(arg)) - footprints += ((footprint, idx + 1)) - - // Then, for each argument to a polymorphic parameter: - // - check formal type via checkType - // - check that hidden set of argument does not overlap with current footprint - // - add footprint of the deep capture set of actual type of argument - // to global footprint(s) - for (arg, idx) <- indexedArgs do + val (qual, fnCaptures) = methPart(fn) match + case Select(qual, _) => (qual, qual.nuType.captureSet) + case _ => (fn, CaptureSet.empty) + var currentPeaks = PeaksPair(fnCaptures.elems.peaks, emptyRefs) + val peaksOfTree: Map[Tree, PeaksPair] = + ((qual -> currentPeaks) :: args.map: arg => + arg -> PeaksPair( + captures(arg).peaks, + if arg.needsSepCheck then formalCaptures(arg).hiddenSet.peaks else emptyRefs) + ).toMap + capt.println( + i"""check separate $fn($args), fnCaptures = $fnCaptures, + | formalCaptures = ${args.map(arg => CaptureSet(formalCaptures(arg)))}, + | actualCaptures = ${args.map(arg => CaptureSet(captures(arg)))}, + | formalPeaks = ${peaksOfTree.values.map(_.formal).toList} + | actualPeaks = ${peaksOfTree.values.map(_.actual).toList} + | deps = ${deps.toList}""") + val parts = qual :: args + + for arg <- args do + val argPeaks = peaksOfTree(arg) + val argDeps = deps(arg) + + def clashingPart(argPeaks: Refs, selector: PeaksPair => Refs): Tree = + parts.iterator.takeWhile(_ ne arg).find: prev => + !argDeps.contains(prev) + && !selector(peaksOfTree(prev)).sharedWith(argPeaks).isEmpty + .getOrElse(EmptyTree) + + // 1. test argPeaks.actual against previously captured formals + if !argPeaks.actual.sharedWith(currentPeaks.formal).isEmpty then + val clashing = clashingPart(argPeaks.actual, _.formal) + if !clashing.isEmpty then sepApplyError(fn, parts, clashing, arg) + else assert(!argDeps.isEmpty) + if arg.needsSepCheck then - val ac = formalCaptures(arg) + //println(i"testing $arg, ${argPeaks.actual}/${argPeaks.formal} against ${currentPeaks.actual}") checkType(arg.formalType, arg.srcPos, TypeRole.Argument(arg)) - val hiddenInArg = ac.hidden.footprint - //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") - val overlap = hiddenInArg.overlapWith(footprint).deductCapturesOf(deps(arg)) - if !overlap.isEmpty then - sepApplyError(fn, args, idx, overlap, hiddenInArg, footprints.toList, deps) - footprint ++= captures(arg).footprint - footprints += ((footprint, idx + 1)) + // 2. test argPeaks.formal against previously hidden actuals + if !argPeaks.formal.sharedWith(currentPeaks.actual).isEmpty then + val clashing = clashingPart(argPeaks.formal, _.actual) + if !clashing.isEmpty then + if !clashing.needsSepCheck then + // if clashing needs a separation check then we already got an erro + // in (1) at position of clashing. No need to report it twice. + //println(i"CLASH $arg / ${argPeaks.formal} vs $clashing / ${peaksOfTree(clashing).actual} / ${captures(clashing).peaks}") + sepApplyError(fn, parts, arg, clashing) + else assert(!argDeps.isEmpty) + + currentPeaks = PeaksPair( + currentPeaks.actual ++ argPeaks.actual, + currentPeaks.formal ++ argPeaks.formal) end checkApply /** The def/use overlap between the references `hiddenByDef` hidden by @@ -757,7 +825,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case dep: TermParamRef => argMap(dep.binder)(dep.paramNum) :: Nil case dep: ThisType if dep.cls == fn.symbol.owner => - val Select(qual, _) = fn: @unchecked + val Select(qual, _) = fn: @unchecked // TODO can we use fn instead? qual :: Nil case _ => Nil diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index d7560c96bd9a..ef100ae94f66 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -442,7 +442,8 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" case Fresh.Cap(hidden) => - if printFreshDetailed then s"" + val idStr = if showUniqueIds then s"#${hidden.id}" else "" + if printFreshDetailed then s"" else if printFresh then "fresh" else "cap" case tp => toText(tp) diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index a807452e5260..714e3a5fc0d6 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -18,6 +18,10 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { var acc: SimpleIdentitySet[B] = SimpleIdentitySet.empty foreach(x => acc += f(x)) acc + def flatMap[B <: AnyRef](f: Elem => SimpleIdentitySet[B]): SimpleIdentitySet[B] = + var acc: SimpleIdentitySet[B] = SimpleIdentitySet.empty + foreach(x => acc ++= f(x)) + acc def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A def toList: List[Elem] def nth(n: Int): Elem diff --git a/tests/neg-custom-args/captures/filevar-expanded.check b/tests/neg-custom-args/captures/filevar-expanded.check index e1991890f6fa..f48736984a36 100644 --- a/tests/neg-custom-args/captures/filevar-expanded.check +++ b/tests/neg-custom-args/captures/filevar-expanded.check @@ -1,19 +1,14 @@ --- Error: tests/neg-custom-args/captures/filevar-expanded.scala:34:19 -------------------------------------------------- +-- Error: tests/neg-custom-args/captures/filevar-expanded.scala:34:13 -------------------------------------------------- 34 | withFile(io3): f => // error: separation failure - | ^ - | Separation failure: argument of type (f: test2.File^{io3}) ->{io3} Unit - | to method withFile: [T](io2: test2.IO^)(op: (f: test2.File^{io2}) => T): T - | corresponds to capture-polymorphic formal parameter op of type (f: test2.File^{io3}) => Unit - | and captures {io3}, but this capability is also passed separately - | in the first argument with type (io3 : test2.IO^). + | ^^^ + | Separation failure: argument of type (io3 : test2.IO^) + | to method withFile: [T](io2: test2.IO^)(op: (f: test2.File^{io2}) => T): T + | corresponds to capture-polymorphic formal parameter io2 of type test2.IO^ + | and hides capabilities {io3}. + | Some of these overlap with the captures of the second argument with type (f: test2.File^{io3}) ->{io3} Unit. | - | Capture set of first argument : {io3} - | Hidden set of current argument : {io3} - | Footprint of first argument : {io3} - | Hidden footprint of current argument : {io3} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {io3} -35 | val o = Service(io3) -36 | o.file = f // this is a bit dubious. It's legal since we treat class refinements -37 | // as capture set variables that can be made to include refs coming from outside. -38 | o.log + | Hidden set of current argument : {io3} + | Hidden footprint of current argument : {io3} + | Capture set of second argument : {io3} + | Footprint set of second argument : {io3} + | The two sets overlap at : {io3} diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index be8f5e56fb87..5a8107151cb4 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -32,12 +32,11 @@ |Separation failure: argument of type (x: Int) ->{cap2} Int |to method map: [U](f: T => U): LazyRef[U]^{f, LazyRef.this} |corresponds to capture-polymorphic formal parameter f of type Int => Int - |and captures {cap2}, but this capability is also passed separately - |in the function prefix with type (LazyRef[Int]{val elem: () ->{ref2*} Int} | (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}))^{ref2}. + |and hides capabilities {cap2}. + |Some of these overlap with the captures of the function prefix with type (LazyRef[Int]{val elem: () ->{ref2*} Int} | (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}))^{ref2}. | - | Capture set of function prefix : {ref1, ref2, ref2*} | Hidden set of current argument : {cap2} - | Footprint of function prefix : {ref1, ref2, ref2*, cap1, cap2} | Hidden footprint of current argument : {cap2} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {cap2} + | Capture set of function prefix : {ref1, ref2, ref2*} + | Footprint set of function prefix : {ref1, ref2, ref2*, cap1, cap2} + | The two sets overlap at : {cap2} diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 48544cb38829..0f17402332f5 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -61,36 +61,34 @@ | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/reaches.scala:80:31 ----------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/reaches.scala:80:28 ----------------------------------------------------------- 80 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck - | ^ - | Separation failure: argument of type (x$0: A) ->{y} box A^? - | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C - | corresponds to capture-polymorphic formal parameter g of type box A^? => box A^? - | and captures {ps*}, but this capability is also passed separately - | in the first argument with type (x$0: A) ->{x} box A^?. + | ^ + | Separation failure: argument of type (x$0: A) ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type (x$0: A) ->{y} box A^?. | - | Capture set of first argument : {x} - | Hidden set of current argument : {y} - | Footprint of first argument : {x, ps*} - | Hidden footprint of current argument : {y, ps*} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {ps*} --- Error: tests/neg-custom-args/captures/reaches.scala:83:31 ----------------------------------------------------------- + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} +-- Error: tests/neg-custom-args/captures/reaches.scala:83:28 ----------------------------------------------------------- 83 | ps.map((x, y) => compose1(x, y)) // error sepcheck - | ^ - | Separation failure: argument of type (x$0: A) ->{y} box A^? - | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C - | corresponds to capture-polymorphic formal parameter g of type box A^? => box A^? - | and captures {ps*}, but this capability is also passed separately - | in the first argument with type (x$0: A) ->{x} box A^?. + | ^ + | Separation failure: argument of type (x$0: A) ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type (x$0: A) ->{y} box A^?. | - | Capture set of first argument : {x} - | Hidden set of current argument : {y} - | Footprint of first argument : {x, ps*} - | Hidden footprint of current argument : {y, ps*} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {ps*} + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} -- Error: tests/neg-custom-args/captures/reaches.scala:62:31 ----------------------------------------------------------- 62 | val leaked = usingFile[File^{id*}]: f => // error | ^^^ diff --git a/tests/neg-custom-args/captures/reaches2.check b/tests/neg-custom-args/captures/reaches2.check index 74223963dde9..c792d3c3a47d 100644 --- a/tests/neg-custom-args/captures/reaches2.check +++ b/tests/neg-custom-args/captures/reaches2.check @@ -8,18 +8,17 @@ | ^ |reference ps* is not included in the allowed capture set {} |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? --- Error: tests/neg-custom-args/captures/reaches2.scala:10:31 ---------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/reaches2.scala:10:28 ---------------------------------------------------------- 10 | ps.map((x, y) => compose1(x, y)) // error // error // error - | ^ - | Separation failure: argument of type (x$0: A) ->{y} box A^? - | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C - | corresponds to capture-polymorphic formal parameter g of type box A^? => box A^? - | and captures {ps*}, but this capability is also passed separately - | in the first argument with type (x$0: A) ->{x} box A^?. + | ^ + | Separation failure: argument of type (x$0: A) ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type (x$0: A) ->{y} box A^?. | - | Capture set of first argument : {x} - | Hidden set of current argument : {y} - | Footprint of first argument : {x, ps*} - | Hidden footprint of current argument : {y, ps*} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {ps*} + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} diff --git a/tests/neg-custom-args/captures/sep-compose.check b/tests/neg-custom-args/captures/sep-compose.check index d763a180b9ed..305887f6b997 100644 --- a/tests/neg-custom-args/captures/sep-compose.check +++ b/tests/neg-custom-args/captures/sep-compose.check @@ -1,120 +1,112 @@ --- Error: tests/neg-custom-args/captures/sep-compose.scala:32:10 ------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/sep-compose.scala:32:7 -------------------------------------------------------- 32 | seq3(f)(f) // error - | ^ - | Separation failure: argument of type (f : () ->{a} Unit) - | to method seq3: (x: () => Unit)(y: () ->{a, cap} Unit): Unit - | corresponds to capture-polymorphic formal parameter y of type () ->{a, cap} Unit - | and captures {f, a, io}, but these capabilities are also passed separately - | in the first argument with type (f : () ->{a} Unit). + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq3: (x: () => Unit)(y: () ->{a, cap} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). | - | Capture set of first argument : {f} - | Hidden set of current argument : {f} - | Footprint of first argument : {f, a, io} - | Hidden footprint of current argument : {f, a, io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {f, a, io} --- Error: tests/neg-custom-args/captures/sep-compose.scala:33:10 ------------------------------------------------------- + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:33:7 -------------------------------------------------------- 33 | seq4(f)(f) // error - | ^ - | Separation failure: argument of type (f : () ->{a} Unit) - | to method seq4: (x: () ->{a, cap} Unit)(y: () => Unit): Unit - | corresponds to capture-polymorphic formal parameter y of type () => Unit - | and captures {f, a, io}, but these capabilities are also passed separately - | in the first argument with type (f : () ->{a} Unit). + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq4: (x: () ->{a, cap} Unit)(y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () ->{a, cap} Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). | - | Capture set of first argument : {f} - | Hidden set of current argument : {f} - | Footprint of first argument : {f, a, io} - | Hidden footprint of current argument : {f, a, io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {f, a, io} --- Error: tests/neg-custom-args/captures/sep-compose.scala:34:10 ------------------------------------------------------- + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:34:7 -------------------------------------------------------- 34 | seq5(f)(f) // error - | ^ - | Separation failure: argument of type (f : () ->{a} Unit) - | to method seq5: (x: () => Unit)(y: () => Unit): Unit - | corresponds to capture-polymorphic formal parameter y of type () => Unit - | and captures {f, a, io}, but these capabilities are also passed separately - | in the first argument with type (f : () ->{a} Unit). + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq5: (x: () => Unit)(y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). | - | Capture set of first argument : {f} - | Hidden set of current argument : {f} - | Footprint of first argument : {f, a, io} - | Hidden footprint of current argument : {f, a, io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {f, a, io} --- Error: tests/neg-custom-args/captures/sep-compose.scala:35:10 ------------------------------------------------------- + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:35:7 -------------------------------------------------------- 35 | seq6(f, f) // error - | ^ - | Separation failure: argument of type (f : () ->{a} Unit) - | to method seq6: (x: () => Unit, y: () ->{a, cap} Unit): Unit - | corresponds to capture-polymorphic formal parameter y of type () ->{a, cap} Unit - | and captures {f, a, io}, but these capabilities are also passed separately - | in the first argument with type (f : () ->{a} Unit). + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq6: (x: () => Unit, y: () ->{a, cap} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). | - | Capture set of first argument : {f} - | Hidden set of current argument : {f} - | Footprint of first argument : {f, a, io} - | Hidden footprint of current argument : {f, a, io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {f, a, io} --- Error: tests/neg-custom-args/captures/sep-compose.scala:36:10 ------------------------------------------------------- + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:36:7 -------------------------------------------------------- 36 | seq7(f, f) // error - | ^ - | Separation failure: argument of type (f : () ->{a} Unit) - | to method seq7: (x: () ->{a, cap} Unit, y: () => Unit): Unit - | corresponds to capture-polymorphic formal parameter y of type () => Unit - | and captures {f, a, io}, but these capabilities are also passed separately - | in the first argument with type (f : () ->{a} Unit). + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq7: (x: () ->{a, cap} Unit, y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () ->{a, cap} Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). | - | Capture set of first argument : {f} - | Hidden set of current argument : {f} - | Footprint of first argument : {f, a, io} - | Hidden footprint of current argument : {f, a, io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {f, a, io} + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} -- Error: tests/neg-custom-args/captures/sep-compose.scala:37:7 -------------------------------------------------------- 37 | seq8(f)(f) // error | ^ | Separation failure: argument of type (f : () ->{a} Unit) | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit | corresponds to capture-polymorphic formal parameter x of type () => Unit - | and captures {f, a, io}, but these capabilities are also passed separately - | in the second argument with type (f : () ->{a} Unit). + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). | - | Capture set of second argument : {f} | Hidden set of current argument : {f} - | Footprint of second argument : {f, a, io} | Hidden footprint of current argument : {f, a, io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} -- Error: tests/neg-custom-args/captures/sep-compose.scala:40:5 -------------------------------------------------------- 40 | p1(f) // error | ^ | Separation failure: argument of type (f : () ->{a} Unit) | to a function of type (x$0: () => Unit) ->{f} Unit | corresponds to capture-polymorphic formal parameter x$0 of type () => Unit - | and captures {f, a, io}, but these capabilities are also passed separately - | in the function prefix. + | and hides capabilities {f}. + | Some of these overlap with the captures of the function prefix. | - | Capture set of function prefix : {p1} | Hidden set of current argument : {f} - | Footprint of function prefix : {p1, f, a, io} | Hidden footprint of current argument : {f, a, io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {f, a, io} + | Capture set of function prefix : {p1} + | Footprint set of function prefix : {p1, f, a, io} + | The two sets overlap at : {f, a, io} -- Error: tests/neg-custom-args/captures/sep-compose.scala:41:38 ------------------------------------------------------- 41 | val p8 = (x: () ->{a} Unit) => seq8(f)(x) // error | ^ - | Separation failure: argument of type (f : () ->{a} Unit) - | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit - | corresponds to capture-polymorphic formal parameter x of type () => Unit - | and captures {a, io}, but these capabilities are also passed separately - | in the second argument with type (x : () ->{a} Unit). + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (x : () ->{a} Unit). | - | Capture set of second argument : {x} - | Hidden set of current argument : {f} - | Footprint of second argument : {x, a, io} - | Hidden footprint of current argument : {f, a, io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {a, io} + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {x} + | Footprint set of second argument : {x, a, io} + | The two sets overlap at : {a, io} diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala index 48f2a84c6fe4..10024724947e 100644 --- a/tests/neg-custom-args/captures/sep-use2.scala +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -15,7 +15,7 @@ def test1(@consume c: Object^, f: Object^ => Object^) = def test2(@consume c: Object^, f: Object^ ->{c} Object^) = def cc: Object^ = c // error val x1 = - { f(cc) } // error // error + { f(cc) } // error val x4: Object^ = { f(c) } // error // error diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check index 45d3553a77d3..3af6c4ea161a 100644 --- a/tests/neg-custom-args/captures/sepchecks2.check +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -4,21 +4,20 @@ | Separation failure: Illegal access to {c} which is hidden by the previous definition | of value xs with type List[box () => Unit]. | This type hides capabilities {xs*, c} --- Error: tests/neg-custom-args/captures/sepchecks2.scala:13:33 -------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:13:7 --------------------------------------------------------- 13 | foo((() => println(c)) :: Nil, c) // error - | ^ - | Separation failure: argument of type (c : Object^) - | to method foo: (xs: List[box () => Unit], y: Object^): Nothing - | corresponds to capture-polymorphic formal parameter y of type Object^ - | and captures {c}, but this capability is also passed separately - | in the first argument with type List[box () ->{c} Unit]. + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: argument of type List[box () ->{c} Unit] + | to method foo: (xs: List[box () => Unit], y: Object^): Nothing + | corresponds to capture-polymorphic formal parameter xs of type List[box () => Unit] + | and hides capabilities {*, c}. + | Some of these overlap with the captures of the second argument with type (c : Object^). | - | Capture set of first argument : {c} - | Hidden set of current argument : {c} - | Footprint of first argument : {c} - | Hidden footprint of current argument : {c} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {c} + | Hidden set of current argument : {*, c} + | Hidden footprint of current argument : {*, c} + | Capture set of second argument : {c} + | Footprint set of second argument : {c} + | The two sets overlap at : {c} -- Error: tests/neg-custom-args/captures/sepchecks2.scala:14:10 -------------------------------------------------------- 14 | val x1: (Object^, Object^) = (c, c) // error | ^^^^^^^^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/sepchecks4.check b/tests/neg-custom-args/captures/sepchecks4.check index 28f7ec55b944..5934be1ff637 100644 --- a/tests/neg-custom-args/captures/sepchecks4.check +++ b/tests/neg-custom-args/captures/sepchecks4.check @@ -8,18 +8,17 @@ | ^^^^^^^^^^ | Separation failure: method bad's result type () => Unit hides parameter io. | The parameter needs to be annotated with @consume to allow this. --- Error: tests/neg-custom-args/captures/sepchecks4.scala:12:25 -------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/sepchecks4.scala:12:6 --------------------------------------------------------- 12 | par(() => println(io))(() => println(io)) // error // (1) - | ^^^^^^^^^^^^^^^^^ - | Separation failure: argument of type () ->{io} Unit - | to method par: (op1: () => Unit)(op2: () => Unit): Unit - | corresponds to capture-polymorphic formal parameter op2 of type () => Unit - | and captures {io}, but this capability is also passed separately - | in the first argument with type () ->{io} Unit. + | ^^^^^^^^^^^^^^^^^ + | Separation failure: argument of type () ->{io} Unit + | to method par: (op1: () => Unit)(op2: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter op1 of type () => Unit + | and hides capabilities {io}. + | Some of these overlap with the captures of the second argument with type () ->{io} Unit. | - | Capture set of first argument : {io} - | Hidden set of current argument : {io} - | Footprint of first argument : {io} - | Hidden footprint of current argument : {io} - | Declared footprint of current argument: {} - | Undeclared overlap of footprints : {io} + | Hidden set of current argument : {io} + | Hidden footprint of current argument : {io} + | Capture set of second argument : {io} + | Footprint set of second argument : {io} + | The two sets overlap at : {io} From 20df57b0f837078b5037507c7c41a38e97f6ba5d Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Feb 2025 12:59:11 +0100 Subject: [PATCH 36/93] Allow more than one existential per binder. In a type like () -> (Ref^, Ref^) we need to map the two caps to different existentially bound variables. One could do it with two binders, like this: () -> (ex1: Exists) -> (ex2: Exists) -> (Ref^{ex1}, Ref^{ex2}) But that's impractical since we need to guess the number of binders needed for inferred types where the `cap` occurrences are inferred late. We therefore keep a single binder but use annotated types for the actual references, like this: () -> (ex: Exists) -> (Ref^{ex @existential}, Ref^{ex @existential}) Each occurrence of @existential is a fresh annotation. Therefore, the two capabilities in the previous example count as different. We need to keep the distinction in one-to-one mappings between Fresh.Cap and existentials. TODO: Extend hidden sets and separation checking to existentials. Right now, subtyping identifies existentials on the same level as mutually subsuming references. This is incorrect, we need to be more precise here. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 5 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 4 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 34 ++++++--- .../src/dotty/tools/dotc/cc/Existential.scala | 60 +++++++++++++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 10 ++- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../dotty/tools/dotc/core/TypeComparer.scala | 21 ++++-- .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../tools/dotc/printing/PlainPrinter.scala | 1 + .../dotty/tools/dotc/transform/Recheck.scala | 1 - library/src/scala/caps.scala | 17 ++++- .../captures/cc-ex-conformance.check | 69 +++++++++++++++++++ .../captures/cc-ex-conformance.scala | 37 +++++----- 13 files changed, 212 insertions(+), 50 deletions(-) create mode 100644 tests/neg-custom-args/captures/cc-ex-conformance.check diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 3d84a37d703b..8f5320bcde7d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -194,8 +194,8 @@ extension (tp: Type) * - annotated types that represent reach or maybe capabilities */ final def isTrackableRef(using Context): Boolean = tp match - case _: (ThisType | TermParamRef) => - true + case _: ThisType => true + case tp: TermParamRef => !Existential.isBinder(tp) case tp: TermRef => ((tp.prefix eq NoPrefix) || tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef @@ -205,6 +205,7 @@ extension (tp: Type) tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet) case tp: TypeParamRef => tp.derivesFrom(defn.Caps_CapSet) + case Existential.Var(_) => true case AnnotatedType(parent, annot) => defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef case _ => diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 6c4d8880636e..779513b5dfcf 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -110,7 +110,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 Existential.Var(_) => true case Fresh.Cap(_) => true case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false @@ -228,8 +228,8 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false || this.match case ReachCapability(x1) => x1.subsumes(y.stripReach) + case Existential.Var(bv) => subsumesExistentially(bv, y) case x: TermRef => viaInfo(x.info)(subsumingRefs(_, y)) - case x: TermParamRef => subsumesExistentially(x, y) case x: TypeRef if assumedContainsOf(x).contains(y) => true case x: TypeRef if x.derivesFrom(defn.Caps_CapSet) => x.info match diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 0fe1b762cb56..cb638d9cfa95 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -103,7 +103,9 @@ sealed abstract class CaptureSet extends Showable: elems.exists(_.stripReadOnly.isCap) final def isUnboxable(using Context) = - elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) + elems.exists: + case Existential.Var(_) => true + case elem => elem.isRootCapability final def isReadOnly(using Context): Boolean = elems.forall(_.isReadOnly) @@ -428,7 +430,10 @@ object CaptureSet: def apply(elems: CaptureRef*)(using Context): CaptureSet.Const = if elems.isEmpty then empty - else Const(SimpleIdentitySet(elems.map(_.ensuring(_.isTrackableRef))*)) + else + for elem <- elems do + assert(elem.isTrackableRef, i"not a trackable ref: $elem") + Const(SimpleIdentitySet(elems*)) def apply(elems: Refs)(using Context): CaptureSet.Const = if elems.isEmpty then empty else Const(elems) @@ -563,12 +568,12 @@ object CaptureSet: private def levelOK(elem: CaptureRef)(using Context): Boolean = if elem.isRootCapability then !noUniversal - else if Existential.isExistentialVar(elem) then - !noUniversal - && !TypeComparer.isOpenedExistential(elem) - // Opened existentials on the left cannot be added to nested capture sets on the right - // of a comparison. Test case is open-existential.scala. else elem match + case Existential.Var(bv) => + !noUniversal + && !TypeComparer.isOpenedExistential(bv) + // Opened existentials on the left cannot be added to nested capture sets on the right + // of a comparison. Test case is open-existential.scala. case elem: TermRef if level.isDefined => elem.prefix match case prefix: CaptureRef => @@ -621,10 +626,13 @@ object CaptureSet: computingApprox = true try val approx = computeApprox(origin).ensuring(_.isConst) - if approx.elems.exists(Existential.isExistentialVar(_)) then + if approx.elems.exists: + case Existential.Var(_) => true + case _ => false + then ccState.approxWarnings += em"""Capture set variable $this gets upper-approximated - |to existential variable from $approx, using {cap} instead.""" + |to existential variable from $approx, using {cap} instead.""" universal else approx finally computingApprox = false @@ -1169,6 +1177,8 @@ object CaptureSet: try pred finally seen -= ref else false + override def toString = "open varState" + object VarState: /** A class for states that do not allow to record elements or dependent sets. @@ -1181,6 +1191,7 @@ object CaptureSet: override def putElems(v: Var, refs: Refs) = false override def putDeps(v: Var, deps: Deps) = false override def isOpen = false + override def toString = "closed varState" /** A closed state that allows a Fresh.Cap instance to subsume a * reference `r` only if `r` is already present in the hidden set of the instance. @@ -1189,6 +1200,7 @@ object CaptureSet: @sharable object Separate extends Closed: override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = false + override def toString = "separating varState" /** A special state that turns off recording of elements. Used only * in `addSub` to prevent cycles in recordings. @@ -1199,6 +1211,7 @@ object CaptureSet: override def putDeps(v: Var, deps: Deps) = true override def rollBack(): Unit = () override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true + override def toString = "unrecorded varState" /** A closed state that turns off recording of hidden elements (but allows * adding them). Used in `mightAccountFor`. @@ -1206,6 +1219,7 @@ object CaptureSet: @sharable private[CaptureSet] object ClosedUnrecorded extends Closed: override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true + override def toString = "closed unrecorded varState" end VarState @@ -1282,6 +1296,8 @@ object CaptureSet: case tp: (TypeRef | TypeParamRef) => if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet else empty + case tp @ Existential.Var(_) => + tp.captureSet case CapturingType(parent, refs) => recur(parent) ++ refs case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) => diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 9aa8793e0889..8ecb4570fcba 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -11,7 +11,10 @@ import typer.ErrorReporting.errorType import Names.TermName import NameKinds.ExistentialBinderName import NameOps.isImpureFunction +import CaptureSet.IdempotentCaptRefMap import reporting.Message +import util.EqHashMap +import util.Spans.NoSpan /** @@ -229,6 +232,19 @@ object Existential: def apply(mk: TermParamRef => Type)(using Context): Type = exMethodType(mk).toFunctionType(alwaysDependent = true) + /** The (super-) type of existentially bound references */ + type Var = AnnotatedType + + /** An extractor for existentially bound references of the form ex @existential + * where ex is a TermParamRef of type Exists + */ + object Var: + def apply(boundVar: TermParamRef)(using Context): Var = + AnnotatedType(boundVar, Annotation(defn.ExistentialAnnot, NoSpan)) + def unapply(tp: Var)(using Context): Option[TermParamRef] = tp match + case AnnotatedType(bv: TermParamRef, ann) if ann.symbol == defn.ExistentialAnnot => Some(bv) + case _ => None + /** Create existential if bound variable appears in result of `mk` */ def wrap(mk: TermParamRef => Type)(using Context): Type = val mt = exMethodType(mk) @@ -242,10 +258,23 @@ object Existential: case _ => core + /** Map existentially bound references referring to `boundVar` one-to-one + * to Fresh.Cap instances + */ + def boundVarToCap(boundVar: TermParamRef, tp: Type)(using Context) = + val subst = new IdempotentCaptRefMap: + val seen = EqHashMap[Annotation, CaptureRef]() + def apply(t: Type): Type = t match + case t @ Var(`boundVar`) => + seen.getOrElseUpdate(t.annot, Fresh.Cap(NoSymbol)) + case _ => + mapOver(t) + subst(tp) + /** Map top-level existentials to `Fresh.Cap`. */ def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - unpacked.substParam(boundVar, Fresh.Cap(NoSymbol)) + boundVarToCap(boundVar, unpacked) case tp1 @ CapturingType(parent, refs) => tp1.derivedCapturingType(toCap(parent), refs) case tp1 @ AnnotatedType(parent, ann) => @@ -256,7 +285,7 @@ object Existential: */ def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - toCapDeeply(unpacked.substParam(boundVar, Fresh.Cap(NoSymbol))) + toCapDeeply(boundVarToCap(boundVar, unpacked)) case tp1 @ FunctionOrMethod(args, res) => val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) if tp2 ne tp1 then tp2 else tp @@ -293,11 +322,13 @@ object Existential: super.mapOver(t) class Wrap(boundVar: TermParamRef) extends CapMap: + private val seen = EqHashMap[CaptureRef, Var]() + def apply(t: Type) = t match - case t: CaptureRef if t.isCapOrFresh => // !!! we should map different fresh refs to different existentials + case t: CaptureRef if t.isCapOrFresh => if variance > 0 then needsWrap = true - boundVar + seen.getOrElseUpdate(t, Var(boundVar)) else if variance == 0 then fail(em"""$tp captures the root capability `cap` in invariant position""") @@ -310,16 +341,27 @@ object Existential: if variance > 0 then needsWrap = true super.mapOver: - defn.FunctionNOf(args, res, contextual).capturing(boundVar.singletonCaptureSet) + defn.FunctionNOf(args, res, contextual) + .capturing(Var(boundVar).singletonCaptureSet) else mapOver(t) case _ => mapOver(t) //.showing(i"mapcap $t = $result") lazy val inverse = new BiTypeMap: - lazy val freshCap = Fresh.Cap(NoSymbol) def apply(t: Type) = t match - case t: TermParamRef if t eq boundVar => freshCap + case t @ Var(`boundVar`) => + // do a reverse getOrElseUpdate on `seen` to produce the + // `Fresh.Cap` assosicated with `t` + val it = seen.iterator + var ref: CaptureRef | Null = null + while it.hasNext && ref == null do + val (k, v) = it.next + if v.annot eq t.annot then ref = k + if ref == null then + ref = Fresh.Cap(NoSymbol) + seen(ref) = t + ref case _ => mapOver(t) def inverse = Wrap.this override def toString = "Wrap.inverse" @@ -359,8 +401,8 @@ object Existential: case (info: TypeRef) :: rest => info.symbol == defn.Caps_Exists && rest.isEmpty case _ => false - /** Is `ref` this an existentially bound variable? */ - def isExistentialVar(ref: CaptureRef)(using Context) = ref match + /** Is `ref` a TermParamRef representing existentially bound variables? */ + def isBinder(ref: CaptureRef)(using Context) = ref match case ref: TermParamRef => isExistentialMethod(ref.binder) case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 86102a172704..56aeac274164 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -376,7 +376,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: checkSharedOK: CapturingType(parent2, ann.tree.toCaptureSet) catch case ex: IllegalCaptureRef => - report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) + if !tptToCheck.isEmpty then + report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) parent2 else if ann.symbol == defn.UncheckedCapturesAnnot then makeUnchecked(apply(parent)) @@ -917,7 +918,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: var retained = ann.retainedElems.toArray for i <- 0 until retained.length do val refTree = retained(i) - for ref <- refTree.toCaptureRefs do + val refs = + try refTree.toCaptureRefs + catch case ex: IllegalCaptureRef => + report.error(em"Illegal capture reference: ${ex.getMessage.nn}", refTree.srcPos) + Nil + for ref <- refs do def pos = if refTree.span.exists then refTree.srcPos else if ann.span.exists then ann.srcPos diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3fba3dbda082..37959f1dc768 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1074,6 +1074,7 @@ class Definitions { @tu lazy val UseAnnot: ClassSymbol = requiredClass("scala.caps.use") @tu lazy val ConsumeAnnot: ClassSymbol = requiredClass("scala.caps.consume") @tu lazy val RefineOverrideAnnot: ClassSymbol = requiredClass("scala.caps.refineOverride") + @tu lazy val ExistentialAnnot: ClassSymbol = requiredClass("scala.caps.existential") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature") @tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter") diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index e4e2c6359eab..99a187a51d70 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2840,14 +2840,21 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def canInstantiateWith(assoc: ExAssoc): Boolean = assoc match case (bv, bvs) :: assoc1 => if bv == tp1 then - !Existential.isExistentialVar(tp2) - || bvs.contains(tp2) - || assoc1.exists(_._1 == tp2) + tp2 match + case Existential.Var(bv2) => + bvs.contains(bv2) || assoc1.exists(_._1 == bv2) + case _ => + true else canInstantiateWith(assoc1) case Nil => false - Existential.isExistentialVar(tp1) && canInstantiateWith(assocExistentials) + tp2 match + case Existential.Var(bv2) if tp1 eq bv2 => + true // for now, existential references referring to the same + // binder are identified. !!! TODO this needs to be revised + case _ => + canInstantiateWith(assocExistentials) def isOpenedExistential(ref: CaptureRef)(using Context): Boolean = openedExistentials.contains(ref) @@ -2864,7 +2871,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling .showing(i"existential match not found for $t in $assoc", capt) def apply(t: Type) = t match - case t: TermParamRef if Existential.isExistentialVar(t) => + case t: TermParamRef if Existential.isBinder(t) => // Find outermost existential on the right that can be instantiated to `t`, // or `badExistential` if none exists. def findMapped(assoc: ExAssoc): CaptureRef = assoc match @@ -2884,7 +2891,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling */ lazy val inverse = new BiTypeMap: def apply(t: Type) = t match - case t: TermParamRef if Existential.isExistentialVar(t) => + case t: TermParamRef if Existential.isBinder(t) => assoc.find(_._1 == t) match case Some((_, bvs)) if bvs.nonEmpty => bvs.head case _ => bad(t) @@ -3980,7 +3987,7 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa } override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures $refs1 <:< $refs2, varState = ${vs.toString}") { + traceIndented(i"subcaptures $refs1 <:< $refs2 in ${vs.toString}") { super.subCaptures(refs1, refs2, vs) } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index d6367bd01bf3..ee6ee7bcd908 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6085,7 +6085,7 @@ object Types extends TypeUtils { case tp: TypeAlias => ensureTrackable(tp.alias) case _ => - assert(false, i"not a trackable captureRef ref: $result, ${result.underlyingIterator.toList}") + assert(false, i"not a trackable CaptureRef: $result with underlying ${result.underlyingIterator.toList}") ensureTrackable(result) /** A restriction of the inverse to a function on tracked CaptureRefs */ diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index ef100ae94f66..f054ed7fac3d 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -441,6 +441,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" + case Existential.Var(bv) => toTextRef(bv) case Fresh.Cap(hidden) => val idStr = if showUniqueIds then s"#${hidden.id}" else "" if printFreshDetailed then s"" diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index e8227f759ad4..60c36fdbbbb7 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -19,7 +19,6 @@ import typer.ErrorReporting.{Addenda, NothingToAdd} import config.Printers.recheckr import util.Property import StdNames.nme -import reporting.trace import annotation.constructorOnly import cc.CaptureSet.IdempotentCaptRefMap import annotation.tailrec diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 4444bdf7e5b3..b70c2b3e8940 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -81,13 +81,28 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class consume extends annotation.StaticAnnotation - /** An annotation placed on a refinement created by capture checking. + /** An internal annotation placed on a refinement created by capture checking. * Refinements with this annotation unconditionally override any * info from the parent type, so no intersection needs to be formed. * This could be useful for tracked parameters as well. */ final class refineOverride extends annotation.StaticAnnotation + /** An internal annotation placed on a reference to an existential capability. + * That way, we can distinguish different universal capabilties referring to + * the same binder. For instance, + * + * () -> (Ref^, Ref^) + * + * would be encoded as + * + * () -> (ex: Exists) -> (Ref^{ex @ existential}, Ref^{ex @existential}) + * + * The two capture references are different since they carry two separately + * allocated annotations. + */ + final class existential extends annotation.StaticAnnotation + object unsafe: extension [T](x: T) diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.check b/tests/neg-custom-args/captures/cc-ex-conformance.check new file mode 100644 index 000000000000..1840fb478689 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-ex-conformance.check @@ -0,0 +1,69 @@ +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:12:14 ------------------------------------------------- +12 |type EX1 = () => (c: Exists) => (C^{c}, C^{c}) // error: illegal capture ref + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:14:14 ------------------------------------------------- +14 |type EX2 = () => (c1: Exists) => (c2: Exists) => (C^{c1}, C^{c2}) // error: illegal capture ref + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Illegal capture reference: c2.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:16:14 ------------------------------------------------- +16 |type EX3 = () => (c: Exists) => (x: Object^) => C^{c} // error: illegal capture ref + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:18:14 ------------------------------------------------- +18 |type EX4 = () => (x: Object^) => (c: Exists) => C^{c} // error: illegal capture ref + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:21:11 ------------------------------------------------- +21 | val ex1: EX1 = ??? // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:22:11 ------------------------------------------------- +22 | val ex2: EX2 = ??? // error: illegal capture ref + | ^^^ + | Illegal capture reference: c2.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:23:9 -------------------------------------------------- +23 | val _: EX1 = ex1 // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:24:9 -------------------------------------------------- +24 | val _: EX2 = ex1 // error separation // error: illegal capture ref + | ^^^ + | Illegal capture reference: c2.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:25:9 -------------------------------------------------- +25 | val _: EX1 = ex2 // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:27:11 ------------------------------------------------- +27 | val ex3: EX3 = ??? // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:28:11 ------------------------------------------------- +28 | val ex4: EX4 = ??? // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:29:9 -------------------------------------------------- +29 | val _: EX4 = ex3 // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:30:9 -------------------------------------------------- +30 | val _: EX4 = ex4 // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:31:9 -------------------------------------------------- +31 | val _: EX3 = ex4 // error: type mismatch // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:31:15 ---------------------------- +31 | val _: EX3 = ex4 // error: type mismatch // error: illegal capture ref + | ^^^ + | Found: () ->{ex4} (x: Object^) ->{ex4*} (ex$18: caps.Exists) -> (c: caps.Exists) ->{ex$18} C + | Required: () ->{fresh} (ex$27: caps.Exists) -> (c: caps.Exists) ->{ex$27} (x: Object^) ->{fresh} C + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:24:15 ------------------------------------------------- +24 | val _: EX2 = ex1 // error separation // error: illegal capture ref + | ^^^ + | Separation failure: Illegal access to {ex1} which is hidden by the previous definition + | of value _$1 with type () => (ex$10: caps.Exists) -> (c: caps.Exists) ->{ex$10} (C, C). + | This type hides capabilities {ex1} diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.scala b/tests/neg-custom-args/captures/cc-ex-conformance.scala index 3b685c5f76b1..8f3efc46fb06 100644 --- a/tests/neg-custom-args/captures/cc-ex-conformance.scala +++ b/tests/neg-custom-args/captures/cc-ex-conformance.scala @@ -1,26 +1,31 @@ +// This contains a lot of illegal capture ref errors, which should be treated as +// noise. The problem is that we can't write an existential type by hand, +// sincxe existentially typed variables carry an @existential annotation, which +// can't be written down. The interesting errors are the rest. + import language.experimental.captureChecking -import caps.{Exists, Capability} +import caps.{Exists, Capability, existential} class C -type EX1 = () => (c: Exists) => (C^{c}, C^{c}) +type EX1 = () => (c: Exists) => (C^{c}, C^{c}) // error: illegal capture ref -type EX2 = () => (c1: Exists) => (c2: Exists) => (C^{c1}, C^{c2}) +type EX2 = () => (c1: Exists) => (c2: Exists) => (C^{c1}, C^{c2}) // error: illegal capture ref -type EX3 = () => (c: Exists) => (x: Object^) => C^{c} +type EX3 = () => (c: Exists) => (x: Object^) => C^{c} // error: illegal capture ref -type EX4 = () => (x: Object^) => (c: Exists) => C^{c} +type EX4 = () => (x: Object^) => (c: Exists) => C^{c} // error: illegal capture ref def Test = - val ex1: EX1 = ??? - val ex2: EX2 = ??? - val _: EX1 = ex1 - val _: EX2 = ex1 // error separation - val _: EX1 = ex2 // ok - - val ex3: EX3 = ??? - val ex4: EX4 = ??? - val _: EX4 = ex3 // ok - val _: EX4 = ex4 // error (???) Probably since we also introduce existentials on expansion - val _: EX3 = ex4 // error + val ex1: EX1 = ??? // error: illegal capture ref + val ex2: EX2 = ??? // error: illegal capture ref + val _: EX1 = ex1 // error: illegal capture ref + val _: EX2 = ex1 // error separation // error: illegal capture ref + val _: EX1 = ex2 // error: illegal capture ref + + val ex3: EX3 = ??? // error: illegal capture ref + val ex4: EX4 = ??? // error: illegal capture ref + val _: EX4 = ex3 // error: illegal capture ref + val _: EX4 = ex4 // error: illegal capture ref + val _: EX3 = ex4 // error: type mismatch // error: illegal capture ref From b115ff6b09ad042a4a053b4b255a63e3908273c8 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Feb 2025 15:17:20 +0100 Subject: [PATCH 37/93] Use peaks-based checking for types --- .../src/dotty/tools/dotc/cc/SepCheck.scala | 304 +++++++++--------- tests/neg-custom-args/captures/lazyref.check | 42 ++- tests/neg-custom-args/captures/lazyref.scala | 6 +- .../neg-custom-args/captures/sep-pairs.check | 41 +++ .../neg-custom-args/captures/sep-pairs.scala | 10 +- .../neg-custom-args/captures/sepchecks2.check | 12 +- 6 files changed, 242 insertions(+), 173 deletions(-) create mode 100644 tests/neg-custom-args/captures/sep-pairs.check diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index bdd9d2723e8e..4902fb755953 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -13,6 +13,7 @@ import StdNames.nme import util.{SimpleIdentitySet, EqHashMap, SrcPos} import tpd.* import reflect.ClassTag +import reporting.trace /** The separation checker is a tree traverser that is run after capture checking. * It checks tree nodes for various separation conditions, explained in the @@ -142,7 +143,7 @@ object SepCheck: val EmptyConsumedSet = ConstConsumedSet(Array(), Array()) - case class PeaksPair(actual: Refs, formal: Refs) + case class PeaksPair(actual: Refs, hidden: Refs) class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import checker.* @@ -325,11 +326,17 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: !others.exists(_.covers(ref)) /** Deduct the footprint of `sym` and `sym*` from `refs` */ - private def deductSym(sym: Symbol)(using Context): Refs = + private def deductSymFootprint(sym: Symbol)(using Context): Refs = val ref = sym.termRef if ref.isTrackableRef then refs.deduct(CaptureSet(ref, ref.reach).elems.footprint) else refs + /** Deduct `sym` and `sym*` from `refs` */ + private def deductSymRefs(sym: Symbol)(using Context): Refs = + val ref = sym.termRef + if ref.isTrackableRef then refs.deduct(SimpleIdentitySet(ref, ref.reach)) + else refs + /** Deduct the footprint of all captures of trees in `deps` from `refs` */ private def deductCapturesOf(deps: List[Tree])(using Context): Refs = deps.foldLeft(refs): (refs, dep) => @@ -348,6 +355,20 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: // ---- Error reporting TODO Once these are stabilized, move to messages ----- + def overlapStr(hiddenSet: Refs, clashSet: Refs)(using Context): String = + val hiddenFootprint = hiddenSet.footprint + val clashFootprint = clashSet.footprint + // The overlap of footprints, or, of this empty the set of shared peaks. + // We prefer footprint overlap since it tends to be more informative. + val overlap = hiddenFootprint.overlapWith(clashFootprint) + if !overlap.isEmpty then i"${CaptureSet(overlap)}" + else + val sharedPeaks = hiddenSet.peaks.sharedWith(clashSet.peaks) + assert(!sharedPeaks.isEmpty, i"no overlap for $hiddenSet vs $clashSet") + sharedPeaks.nth(0) match + case fresh @ Fresh.Cap(hidden) => + if hidden.owner.exists then i"cap of ${hidden.owner}" else i"$fresh" + /** Report a separation failure in an application `fn(args)` * @param fn the function * @param parts the function prefix followed by the flattened argument list @@ -385,21 +406,6 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: else i" with type ${clashing.nuType}" val hiddenSet = formalCaptures(polyArg).hiddenSet val clashSet = captures(clashing) - val hiddenFootprint = hiddenSet.footprint - val clashFootprint = clashSet.footprint - val overlapStr = - // The overlap of footprints, or, of this empty the set of shared peaks. - // We prefer footprint overlap since it tends to be more informative. - val overlap = hiddenFootprint.overlapWith(clashFootprint) - if !overlap.isEmpty then i"${CaptureSet(overlap)}" - else - val sharedPeaks = hiddenSet.peaks.sharedWith(clashSet.peaks) - assert(!sharedPeaks.isEmpty, - i"no overlap for $polyArg: $hiddenSet} vs $clashing: $clashSet") - sharedPeaks.nth(0) match - case fresh @ Fresh.Cap(hidden) => - if hidden.owner.exists then i"cap of ${hidden.owner}" else i"$fresh" - report.error( em"""Separation failure: argument of type ${polyArg.nuType} |to $funStr @@ -411,7 +417,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: | Hidden footprint of current argument : ${CaptureSet(hiddenSet.footprint)} | Capture set of $clashArgStr : ${CaptureSet(clashSet)} | Footprint set of $clashArgStr : ${CaptureSet(clashSet.footprint)} - | The two sets overlap at : $overlapStr""", + | The two sets overlap at : ${overlapStr(hiddenSet, clashSet)}""", polyArg.srcPos) /** Report a use/definition failure, where a previously hidden capability is @@ -496,43 +502,41 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case Select(qual, _) => (qual, qual.nuType.captureSet) case _ => (fn, CaptureSet.empty) var currentPeaks = PeaksPair(fnCaptures.elems.peaks, emptyRefs) - val peaksOfTree: Map[Tree, PeaksPair] = - ((qual -> currentPeaks) :: args.map: arg => - arg -> PeaksPair( - captures(arg).peaks, - if arg.needsSepCheck then formalCaptures(arg).hiddenSet.peaks else emptyRefs) - ).toMap + val partsWithPeaks = mutable.ListBuffer[(Tree, PeaksPair)]() += (qual -> currentPeaks) + capt.println( i"""check separate $fn($args), fnCaptures = $fnCaptures, | formalCaptures = ${args.map(arg => CaptureSet(formalCaptures(arg)))}, | actualCaptures = ${args.map(arg => CaptureSet(captures(arg)))}, - | formalPeaks = ${peaksOfTree.values.map(_.formal).toList} - | actualPeaks = ${peaksOfTree.values.map(_.actual).toList} | deps = ${deps.toList}""") val parts = qual :: args for arg <- args do - val argPeaks = peaksOfTree(arg) + val argPeaks = PeaksPair( + captures(arg).peaks, + if arg.needsSepCheck then formalCaptures(arg).hiddenSet.peaks else emptyRefs) val argDeps = deps(arg) def clashingPart(argPeaks: Refs, selector: PeaksPair => Refs): Tree = - parts.iterator.takeWhile(_ ne arg).find: prev => - !argDeps.contains(prev) - && !selector(peaksOfTree(prev)).sharedWith(argPeaks).isEmpty - .getOrElse(EmptyTree) - - // 1. test argPeaks.actual against previously captured formals - if !argPeaks.actual.sharedWith(currentPeaks.formal).isEmpty then - val clashing = clashingPart(argPeaks.actual, _.formal) + partsWithPeaks.find: (prev, prevPeaks) => + !argDeps.contains(prev) + && !selector(prevPeaks).sharedWith(argPeaks).isEmpty + match + case Some(prev, _) => prev + case None => EmptyTree + + // 1. test argPeaks.actual against previously captured hidden sets + if !argPeaks.actual.sharedWith(currentPeaks.hidden).isEmpty then + val clashing = clashingPart(argPeaks.actual, _.hidden) if !clashing.isEmpty then sepApplyError(fn, parts, clashing, arg) else assert(!argDeps.isEmpty) if arg.needsSepCheck then //println(i"testing $arg, ${argPeaks.actual}/${argPeaks.formal} against ${currentPeaks.actual}") checkType(arg.formalType, arg.srcPos, TypeRole.Argument(arg)) - // 2. test argPeaks.formal against previously hidden actuals - if !argPeaks.formal.sharedWith(currentPeaks.actual).isEmpty then - val clashing = clashingPart(argPeaks.formal, _.actual) + // 2. test argPeaks.hidden against previously captured actuals + if !argPeaks.hidden.sharedWith(currentPeaks.actual).isEmpty then + val clashing = clashingPart(argPeaks.hidden, _.actual) if !clashing.isEmpty then if !clashing.needsSepCheck then // if clashing needs a separation check then we already got an erro @@ -541,9 +545,10 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: sepApplyError(fn, parts, arg, clashing) else assert(!argDeps.isEmpty) + partsWithPeaks += (arg -> argPeaks) currentPeaks = PeaksPair( currentPeaks.actual ++ argPeaks.actual, - currentPeaks.formal ++ argPeaks.formal) + currentPeaks.hidden ++ argPeaks.hidden) end checkApply /** The def/use overlap between the references `hiddenByDef` hidden by @@ -590,7 +595,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * - If `tpe` is morally a singleton type deduct it as well. */ def prune(refs: Refs, tpe: Type, role: TypeRole)(using Context): Refs = - refs.deductSym(role.dclSym).deduct(explicitRefs(tpe)) + refs.deductSymFootprint(role.dclSym).deduct(explicitRefs(tpe)) /** Check validity of consumed references `refsToCheck`. The references are consumed * because they are hidden in a Fresh.Cap result type or they are referred @@ -669,55 +674,62 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: */ def checkType(tpe: Type, pos: SrcPos, role: TypeRole)(using Context): Unit = + extension (refs: Refs) def pruned = + refs.deductSymRefs(role.dclSym).deduct(explicitRefs(tpe)) + + def sepTypeError(parts: List[Type], genPart: Type, otherPart: Type): Unit = + val captured = genPart.deepCaptureSet.elems + val hiddenSet = captured.hiddenSet.pruned + val clashSet = otherPart.deepCaptureSet.elems + val deepClashSet = (clashSet.footprint ++ clashSet.hiddenSet).pruned + report.error( + em"""Separation failure in ${role.description} $tpe. + |One part, $genPart, hides capabilities ${CaptureSet(hiddenSet)}. + |Another part, $otherPart, captures capabilities ${CaptureSet(deepClashSet)}. + |The two sets overlap at ${overlapStr(hiddenSet, deepClashSet)}.""", + pos) + /** Check that the parts of type `tpe` are mutually separated. * This means that references hidden in some part of the type may not * be explicitly referenced or hidden in some other part. */ def checkParts(parts: List[Type]): Unit = - var footprint: Refs = emptyRefs - var hiddenSet: Refs = emptyRefs - var checked = 0 - for part <- parts do + var currentPeaks = PeaksPair(emptyRefs, emptyRefs) + val partsWithPeaks = mutable.ListBuffer[(Type, PeaksPair)]() - /** Report an error if `current` and `next` overlap. - * @param current the footprint or hidden set seen so far - * @param next the footprint or hidden set of the next part - * @param mapRefs a function over the capture set elements of the next part - * that returns the references of the same kind as `current` - * (i.e. the part's footprint or hidden set) - * @param prevRel a verbal description of current ("references or "hides") - * @param nextRel a verbal descriiption of next - */ - def checkSep(current: Refs, next: Refs, mapRefs: Refs => Refs, prevRel: String, nextRel: String): Unit = - val globalOverlap = current.overlapWith(next) - if !globalOverlap.isEmpty then - val (prevStr, prevRefs, overlap) = parts.iterator.take(checked) - .map: prev => - val prevRefs = prune(mapRefs(prev.deepCaptureSet.elems).footprint, tpe, role) - (i", $prev , ", prevRefs, prevRefs.overlapWith(next)) - .dropWhile(_._3.isEmpty) - .nextOption - .getOrElse(("", current, globalOverlap)) - val alsoStr = if next == prevRefs && nextRel == prevRel then "also " else "" - report.error( - em"""Separation failure in ${role.description} $tpe. - |One part, $part , $nextRel ${CaptureSet(next)}. - |A previous part$prevStr $alsoStr$prevRel ${CaptureSet(prevRefs)}. - |The two sets overlap at ${CaptureSet(overlap)}.""", - pos) - - val partRefs = part.deepCaptureSet.elems - val partFootprint = prune(partRefs.footprint, tpe, role) - val partHidden = prune(partRefs.hidden.footprint, tpe, role).deduct(partFootprint) - - checkSep(footprint, partHidden, identity, "references", "hides") - checkSep(hiddenSet, partHidden, _.hidden, "hides", "hides") - checkSep(hiddenSet, partFootprint, _.hidden, "hides", "references") - - footprint ++= partFootprint - hiddenSet ++= partHidden - checked += 1 - end for + for part <- parts do + val captured = part.deepCaptureSet.elems.pruned + val hidden = captured.hiddenSet.pruned + val actual = captured ++ hidden + val partPeaks = PeaksPair(actual.peaks, hidden.peaks) + /* + println(i"""check parts $parts + |current = ${currentPeaks.actual}/${currentPeaks.hidden} + |new = $captured/${captured.hiddenSet.pruned} + |new = ${captured.peaks}/${captured.hiddenSet.pruned.peaks}""") + */ + + def clashingPart(argPeaks: Refs, selector: PeaksPair => Refs): Type = + partsWithPeaks.find: (prev, prevPeaks) => + !selector(prevPeaks).sharedWith(argPeaks).isEmpty + match + case Some(prev, _) => prev + case None => NoType + + if !partPeaks.actual.sharedWith(currentPeaks.hidden).isEmpty then + //println(i"CLASH ${partPeaks.actual} with ${currentPeaks.hidden}") + val clashing = clashingPart(partPeaks.actual, _.hidden) + //println(i"CLASH ${partPeaks.actual} with ${currentPeaks.hidden}") + if clashing.exists then sepTypeError(parts, clashing, part) + + if !partPeaks.hidden.sharedWith(currentPeaks.actual).isEmpty then + val clashing = clashingPart(partPeaks.hidden, _.actual) + if clashing.exists then sepTypeError(parts, part, clashing) + + partsWithPeaks += (part -> partPeaks) + currentPeaks = PeaksPair( + currentPeaks.actual ++ partPeaks.actual, + currentPeaks.hidden ++ partPeaks.hidden) end checkParts /** A traverser that collects part lists to check for separation conditions. @@ -858,74 +870,74 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: checkType(tree.tpt, tree.symbol) if previousDefs.nonEmpty then capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hidden.footprint}") - defsShadow ++= captures(tree.tpt).hidden.footprint.deductSym(tree.symbol) + defsShadow ++= captures(tree.tpt).hidden.deductSymRefs(tree.symbol).footprint resultType(tree.symbol) = tree.tpt.nuType previousDefs.head += tree /** Traverse `tree` and perform separation checks everywhere */ def traverse(tree: Tree)(using Context): Unit = - if isUnsafeAssumeSeparate(tree) then return - checkUse(tree) - tree match - case tree @ Select(qual, _) if tree.symbol.is(Method) && tree.symbol.hasAnnotation(defn.ConsumeAnnot) => - traverseChildren(tree) - checkConsumedRefs( - captures(qual).footprint, qual.nuType, - TypeRole.Qualifier(qual, tree.symbol), - i"call prefix of @consume ${tree.symbol} refers to", qual.srcPos) - case tree: GenericApply => - traverseChildren(tree) - tree.tpe match - case _: MethodOrPoly => - case _ => traverseApply(tree, Nil) - case tree: Block => - val saved = defsShadow - previousDefs = mutable.ListBuffer() :: previousDefs - try traverseChildren(tree) - finally - previousDefs = previousDefs.tail - defsShadow = saved - case tree: ValDef => - traverseChildren(tree) - checkValOrDefDef(tree) - case tree: DefDef => - withFreshConsumed: + if !isUnsafeAssumeSeparate(tree) then trace(i"checking separate $tree"): + checkUse(tree) + tree match + case tree @ Select(qual, _) if tree.symbol.is(Method) && tree.symbol.hasAnnotation(defn.ConsumeAnnot) => traverseChildren(tree) - checkValOrDefDef(tree) - case If(cond, thenp, elsep) => - traverse(cond) - val thenConsumed = consumed.segment(traverse(thenp)) - val elseConsumed = consumed.segment(traverse(elsep)) - consumed ++= thenConsumed - consumed ++= elseConsumed - case tree @ Labeled(bind, expr) => - val consumedBuf = mutable.ListBuffer[ConsumedSet]() - openLabeled = (bind.name, consumedBuf) :: openLabeled - traverse(expr) - for cs <- consumedBuf do consumed ++= cs - openLabeled = openLabeled.tail - case Return(expr, from) => - val retConsumed = consumed.segment(traverse(expr)) - from match - case Ident(name) => - for (lbl, consumedBuf) <- openLabeled do - if lbl == name then - consumedBuf += retConsumed - case _ => - case Match(sel, cases) => - // Matches without returns might still be kept after pattern matching to - // encode table switches. - traverse(sel) - val caseConsumed = for cas <- cases yield consumed.segment(traverse(cas)) - caseConsumed.foreach(consumed ++= _) - case tree: TypeDef if tree.symbol.isClass => - withFreshConsumed: + checkConsumedRefs( + captures(qual).footprint, qual.nuType, + TypeRole.Qualifier(qual, tree.symbol), + i"call prefix of @consume ${tree.symbol} refers to", qual.srcPos) + case tree: GenericApply => + traverseChildren(tree) + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree, Nil) + case tree: Block => + val saved = defsShadow + previousDefs = mutable.ListBuffer() :: previousDefs + try traverseChildren(tree) + finally + previousDefs = previousDefs.tail + defsShadow = saved + case tree: ValDef => + traverseChildren(tree) + checkValOrDefDef(tree) + case tree: DefDef => + withFreshConsumed: + traverseChildren(tree) + checkValOrDefDef(tree) + case If(cond, thenp, elsep) => + traverse(cond) + val thenConsumed = consumed.segment(traverse(thenp)) + val elseConsumed = consumed.segment(traverse(elsep)) + consumed ++= thenConsumed + consumed ++= elseConsumed + case tree @ Labeled(bind, expr) => + val consumedBuf = mutable.ListBuffer[ConsumedSet]() + openLabeled = (bind.name, consumedBuf) :: openLabeled + traverse(expr) + for cs <- consumedBuf do consumed ++= cs + openLabeled = openLabeled.tail + case Return(expr, from) => + val retConsumed = consumed.segment(traverse(expr)) + from match + case Ident(name) => + for (lbl, consumedBuf) <- openLabeled do + if lbl == name then + consumedBuf += retConsumed + case _ => + case Match(sel, cases) => + // Matches without returns might still be kept after pattern matching to + // encode table switches. + traverse(sel) + val caseConsumed = for cas <- cases yield consumed.segment(traverse(cas)) + caseConsumed.foreach(consumed ++= _) + case tree: TypeDef if tree.symbol.isClass => + withFreshConsumed: + traverseChildren(tree) + case tree: WhileDo => + val loopConsumed = consumed.segment(traverseChildren(tree)) + if loopConsumed.size != 0 then + val (ref, pos) = loopConsumed.toMap.head + consumeInLoopError(ref, pos) + case _ => traverseChildren(tree) - case tree: WhileDo => - val loopConsumed = consumed.segment(traverseChildren(tree)) - if loopConsumed.size != 0 then - val (ref, pos) = loopConsumed.toMap.head - consumeInLoopError(ref, pos) - case _ => - traverseChildren(tree) end SepCheck \ No newline at end of file diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 5a8107151cb4..22c4680a48fe 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -19,24 +19,34 @@ | Required: LazyRef[Int]^{ref1} | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:26:35 -------------------------------------- -26 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:30:35 -------------------------------------- +30 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref4*} Int}^{ref4} | Required: LazyRef[Int]^{cap1} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/lazyref.scala:25:55 ----------------------------------------------------------- -25 | val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure - | ^ - |Separation failure: argument of type (x: Int) ->{cap2} Int - |to method map: [U](f: T => U): LazyRef[U]^{f, LazyRef.this} - |corresponds to capture-polymorphic formal parameter f of type Int => Int - |and hides capabilities {cap2}. - |Some of these overlap with the captures of the function prefix with type (LazyRef[Int]{val elem: () ->{ref2*} Int} | (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}))^{ref2}. - | - | Hidden set of current argument : {cap2} - | Hidden footprint of current argument : {cap2} - | Capture set of function prefix : {ref1, ref2, ref2*} - | Footprint set of function prefix : {ref1, ref2, ref2*, cap1, cap2} - | The two sets overlap at : {cap2} +-- Error: tests/neg-custom-args/captures/lazyref.scala:26:9 ------------------------------------------------------------ +26 | if cap1 == cap2 // error: separation failure // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap1} which is hidden by the previous definition + | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1, cap2, ref1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:26:17 ----------------------------------------------------------- +26 | if cap1 == cap2 // error: separation failure // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap2} which is hidden by the previous definition + | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1, cap2, ref1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:27:11 ----------------------------------------------------------- +27 | then ref1 // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {ref1} which is hidden by the previous definition + | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1, cap2, ref1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:29:9 ------------------------------------------------------------ +29 | .map(g) // error: separation failure + | ^ + | Separation failure: Illegal access to {cap2} which is hidden by the previous definition + | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1, cap2, ref1} diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 549bcc2257a5..8715188f0ba1 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -22,5 +22,9 @@ 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) // error: separation failure + val ref4 = ( + if cap1 == cap2 // error: separation failure // error: separation failure + then ref1 // error: separation failure + else ref2) + .map(g) // error: separation failure val ref4c: LazyRef[Int]^{cap1} = ref4 // error diff --git a/tests/neg-custom-args/captures/sep-pairs.check b/tests/neg-custom-args/captures/sep-pairs.check new file mode 100644 index 000000000000..265d42e155e8 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-pairs.check @@ -0,0 +1,41 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sep-pairs.scala:32:10 ------------------------------------ +32 | Pair(Ref(), Ref()) // error // error: universal capability cannot be included in capture set + | ^^^^^ + | Found: box Ref^{cap.rd, cap} + | Required: box Ref^? + | + | Note that the universal capability `cap.rd` + | cannot be included in capture set ? + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sep-pairs.scala:32:17 ------------------------------------ +32 | Pair(Ref(), Ref()) // error // error: universal capability cannot be included in capture set + | ^^^^^ + | Found: box Ref^{cap.rd, cap} + | Required: box Ref^? + | + | Note that the universal capability `cap.rd` + | cannot be included in capture set ? + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/sep-pairs.scala:15:10 --------------------------------------------------------- +15 | val r1: Pair[Ref^, Ref^] = mkPair(r0) // error: overlap at r0 + | ^^^^^^^^^^^^^^^^ + | Separation failure in value r1's type Pair[box Ref^, box Ref^]. + | One part, box Ref^, hides capabilities {r0}. + | Another part, box Ref^, captures capabilities {r0}. + | The two sets overlap at {r0}. +-- Error: tests/neg-custom-args/captures/sep-pairs.scala:13:9 ---------------------------------------------------------- +13 |def bad: Pair[Ref^, Ref^] = // error: overlap at r1*, r0 + | ^^^^^^^^^^^^^^^^ + | Separation failure in method bad's result type Pair[box Ref^, box Ref^]. + | One part, box Ref^, hides capabilities {cap, cap}. + | Another part, box Ref^, captures capabilities {cap, cap}. + | The two sets overlap at cap of method bad. +-- Error: tests/neg-custom-args/captures/sep-pairs.scala:44:18 --------------------------------------------------------- +44 | val sameToPair: Pair[Ref^, Ref^] = Pair(fstSame, sndSame) // error + | ^^^^^^^^^^^^^^^^ + | Separation failure in value sameToPair's type Pair[box Ref^, box Ref^]. + | One part, box Ref^, hides capabilities {fstSame}. + | Another part, box Ref^, captures capabilities {sndSame}. + | The two sets overlap at cap of value same. diff --git a/tests/neg-custom-args/captures/sep-pairs.scala b/tests/neg-custom-args/captures/sep-pairs.scala index 4b25e6d61719..582c635ce27d 100644 --- a/tests/neg-custom-args/captures/sep-pairs.scala +++ b/tests/neg-custom-args/captures/sep-pairs.scala @@ -36,10 +36,12 @@ def test(io: Object^): Unit = val two = twoRefs() val fst: Ref^{two.fst*} = two.fst val snd: Ref^{two.snd*} = two.snd + val twoCopy: Pair[Ref^, Ref^] = Pair(fst, snd) // ok + + val same = twoRefs2() + val fstSame = same.fst + val sndSame = same.snd + val sameToPair: Pair[Ref^, Ref^] = Pair(fstSame, sndSame) // error - val two2 = twoRefs2() - val fst2 = two.fst - val snd2 = two.snd - val p2: Pair[Ref^, Ref^] = Pair(fst, snd) // should be error diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check index 3af6c4ea161a..8f66aa5b3f42 100644 --- a/tests/neg-custom-args/captures/sepchecks2.check +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -22,22 +22,22 @@ 14 | val x1: (Object^, Object^) = (c, c) // error | ^^^^^^^^^^^^^^^^^^ | Separation failure in value x1's type (box Object^, box Object^). - | One part, box Object^ , hides {c}. - | A previous part, box Object^ , also hides {c}. + | One part, box Object^, hides capabilities {c}. + | Another part, box Object^, captures capabilities {c}. | The two sets overlap at {c}. -- Error: tests/neg-custom-args/captures/sepchecks2.scala:15:10 -------------------------------------------------------- 15 | val x2: (Object^, Object^{d}) = (d, d) // error | ^^^^^^^^^^^^^^^^^^^^^ | Separation failure in value x2's type (box Object^, box Object^{d}). - | One part, box Object^{d} , references {d}. - | A previous part, box Object^ , hides {d}. + | One part, box Object^, hides capabilities {d}. + | Another part, box Object^{d}, captures capabilities {d}. | The two sets overlap at {d}. -- Error: tests/neg-custom-args/captures/sepchecks2.scala:27:6 --------------------------------------------------------- 27 | bar((c, c)) // error | ^^^^^^ | Separation failure in the argument's adapted type (box Object^, box Object^). - | One part, box Object^ , hides {c}. - | A previous part, box Object^ , also hides {c}. + | One part, box Object^, hides capabilities {c}. + | Another part, box Object^, captures capabilities {c}. | The two sets overlap at {c}. -- Error: tests/neg-custom-args/captures/sepchecks2.scala:30:9 --------------------------------------------------------- 30 | val x: (Object^, Object^{c}) = (d, c) // error From c049774259009beefb1ed5d8d7779668c37f9a38 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Feb 2025 19:51:49 +0100 Subject: [PATCH 38/93] Drop transitive hidden set construction --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 6 ++- .../src/dotty/tools/dotc/cc/SepCheck.scala | 48 ++++++------------- .../neg-custom-args/captures/sep-pairs.check | 6 +-- tests/neg-custom-args/captures/sep-use2.scala | 2 +- 4 files changed, 23 insertions(+), 39 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 49ad33ad53da..9b7dc0455a9c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -525,7 +525,11 @@ class CheckCaptures extends Recheck, SymTransformer: def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => - if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), tree) + def isRetained(ref: CaptureRef): Boolean = ref.pathRoot match + case root: ThisType => ctx.owner.isContainedIn(root.cls) + case _ => true + if sym.exists && curEnv.isOpen then + markFree(capturedVars(sym).filter(isRetained), tree) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 4902fb755953..b0c382af9500 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -197,24 +197,6 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val elems: Refs = refs.filter(!_.isMaxCapability) recur(elems, elems.toList) - /** The members of type Fresh.Cap(...) or Fresh.Cap(...).rd in the transitive closure - * of this set - */ - private def freshElems(using Context): Refs = - def recur(seen: Refs, acc: Refs, newElems: List[CaptureRef]): Refs = newElems match - case newElem :: newElems1 => - if seen.contains(newElem) then - recur(seen, acc, newElems1) - else newElem.stripReadOnly match - case Fresh.Cap(_) => - recur(seen, acc + newElem, newElems1) - //case _: TypeRef | _: TypeParamRef => - // recur(seen + newElem, acc, newElems1) - case _ => - recur(seen + newElem, acc, newElem.captureSetOfInfo.elems.toList ++ newElems1) - case Nil => acc - recur(emptyRefs, emptyRefs, refs.toList) - private def peaks(using Context): Refs = def recur(seen: Refs, acc: Refs, newElems: List[CaptureRef]): Refs = newElems match case newElem :: newElems1 => @@ -286,7 +268,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * capability in `refs`. E g. if `R = {x, >}` then * its hidden set is `{y, z}`. */ - private def hidden(using Context): Refs = + private def hiddenSet(using Context): Refs = val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet def hiddenByElem(elem: CaptureRef): Refs = elem match @@ -299,7 +281,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if seen.add(elem) then elems ++ hiddenByElem(elem) else elems recur(refs) - end hidden + end hiddenSet /** Same as !refs.hidden.isEmpty but more efficient */ private def containsHidden(using Context): Boolean = @@ -315,11 +297,6 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: recur(refs) end containsHidden - def hiddenSet(using Context): Refs = - freshElems.flatMap: - case Fresh.Cap(hidden) => hidden.elems - case ReadOnlyCapability(Fresh.Cap(hidden)) => hidden.elems.map(_.readOnly) - /** Subtract all elements that are covered by some element in `others` from this set. */ private def deduct(others: Refs)(using Context): Refs = refs.filter: ref => @@ -353,7 +330,12 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: private def captures(tree: Tree)(using Context): Refs = tree.nuType.deepCaptureSet.elems - // ---- Error reporting TODO Once these are stabilized, move to messages ----- + // ---- Error reporting TODO Once these are stabilized, move to messages -----" + + + def sharedPeaksStr(shared: Refs)(using Context): String = + shared.nth(0) match + case fresh @ Fresh.Cap(hidden) => + if hidden.owner.exists then i"$fresh of ${hidden.owner}" else i"$fresh" def overlapStr(hiddenSet: Refs, clashSet: Refs)(using Context): String = val hiddenFootprint = hiddenSet.footprint @@ -365,9 +347,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: else val sharedPeaks = hiddenSet.peaks.sharedWith(clashSet.peaks) assert(!sharedPeaks.isEmpty, i"no overlap for $hiddenSet vs $clashSet") - sharedPeaks.nth(0) match - case fresh @ Fresh.Cap(hidden) => - if hidden.owner.exists then i"cap of ${hidden.owner}" else i"$fresh" + sharedPeaksStr(sharedPeaks) /** Report a separation failure in an application `fn(args)` * @param fn the function @@ -429,7 +409,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: */ def sepUseError(tree: Tree, used: Refs, globalOverlap: Refs)(using Context): Unit = val individualChecks = for mdefs <- previousDefs.iterator; mdef <- mdefs.iterator yield - val hiddenByDef = captures(mdef.tpt).hidden.footprint + val hiddenByDef = captures(mdef.tpt).hiddenSet.footprint val overlap = defUseOverlap(hiddenByDef, used, tree.symbol) if !overlap.isEmpty then def resultStr = if mdef.isInstanceOf[DefDef] then " result" else "" @@ -560,7 +540,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: resultType.get(sym) match case Some(tp) if !overlap.isEmpty => val declared = tp.captureSet.elems - overlap.deduct(declared.footprint).deduct(declared.hidden.footprint) + overlap.deduct(declared.footprint).deduct(declared.hiddenSet.footprint) case _ => overlap @@ -784,7 +764,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: // "see through them" when we look at hidden sets. then val refs = tpe.deepCaptureSet.elems - val toCheck = refs.hidden.footprint.deduct(refs.footprint) + val toCheck = refs.hiddenSet.footprint.deduct(refs.footprint) checkConsumedRefs(toCheck, tpe, role, i"${role.description} $tpe hides", pos) case TypeRole.Argument(arg) => if tpe.hasAnnotation(defn.ConsumeAnnot) then @@ -869,8 +849,8 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then checkType(tree.tpt, tree.symbol) if previousDefs.nonEmpty then - capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hidden.footprint}") - defsShadow ++= captures(tree.tpt).hidden.deductSymRefs(tree.symbol).footprint + capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hiddenSet.footprint}") + defsShadow ++= captures(tree.tpt).hiddenSet.deductSymRefs(tree.symbol).footprint resultType(tree.symbol) = tree.tpt.nuType previousDefs.head += tree diff --git a/tests/neg-custom-args/captures/sep-pairs.check b/tests/neg-custom-args/captures/sep-pairs.check index 265d42e155e8..948c822ce5ac 100644 --- a/tests/neg-custom-args/captures/sep-pairs.check +++ b/tests/neg-custom-args/captures/sep-pairs.check @@ -29,9 +29,9 @@ 13 |def bad: Pair[Ref^, Ref^] = // error: overlap at r1*, r0 | ^^^^^^^^^^^^^^^^ | Separation failure in method bad's result type Pair[box Ref^, box Ref^]. - | One part, box Ref^, hides capabilities {cap, cap}. - | Another part, box Ref^, captures capabilities {cap, cap}. - | The two sets overlap at cap of method bad. + | One part, box Ref^, hides capabilities {r1*, r0}. + | Another part, box Ref^, captures capabilities {r1*, r0}. + | The two sets overlap at {r1*, r0}. -- Error: tests/neg-custom-args/captures/sep-pairs.scala:44:18 --------------------------------------------------------- 44 | val sameToPair: Pair[Ref^, Ref^] = Pair(fstSame, sndSame) // error | ^^^^^^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala index 10024724947e..48f2a84c6fe4 100644 --- a/tests/neg-custom-args/captures/sep-use2.scala +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -15,7 +15,7 @@ def test1(@consume c: Object^, f: Object^ => Object^) = def test2(@consume c: Object^, f: Object^ ->{c} Object^) = def cc: Object^ = c // error val x1 = - { f(cc) } // error + { f(cc) } // error // error val x4: Object^ = { f(c) } // error // error From 81491f00ada21ee426ec8f526a06a03d65731a60 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 14 Feb 2025 14:45:14 +0100 Subject: [PATCH 39/93] Peak-based separation checking for def-use --- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 2 + .../src/dotty/tools/dotc/cc/SepCheck.scala | 139 +++++++++--------- library/src/scala/caps.scala | 2 +- .../immutable/LazyListIterable.scala | 14 +- tests/neg-custom-args/captures/lazyref.check | 38 +++-- tests/neg-custom-args/captures/lazyref.scala | 8 +- .../neg-custom-args/captures/sepchecks2.check | 2 +- tests/pos-custom-args/captures/path-use.scala | 6 +- 8 files changed, 119 insertions(+), 92 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 779513b5dfcf..c9fde7323928 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -287,6 +287,8 @@ trait CaptureRef extends TypeProxy, ValueType: this match case MaybeCapability(x1) => x1.covers(y1) case _ => false + case Fresh.Cap(hidden) => + hidden.superCaps.exists(this covers _) case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index b0c382af9500..acb26233dcae 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -145,6 +145,8 @@ object SepCheck: case class PeaksPair(actual: Refs, hidden: Refs) + case class DefInfo(tree: ValOrDefDef, symbol: Symbol, hidden: Refs, hiddenPeaks: Refs) + class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import checker.* import SepCheck.* @@ -154,15 +156,10 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: */ private var defsShadow: Refs = emptyRefs - /** A map from definitions to their internal result types. - * Populated during separation checking traversal. - */ - private val resultType = EqHashMap[Symbol, Type]() - - /** The previous val or def definitions encountered during separation checking. - * These all enclose and precede the current traversal node. + /** The previous val or def definitions encountered during separation checking + * in reverse order. These all enclose and precede the current traversal node. */ - private var previousDefs: List[mutable.ListBuffer[ValOrDefDef]] = Nil + private var previousDefs: List[DefInfo] = Nil /** The set of references that were consumed so far in the current method */ private var consumed: MutConsumedSet = MutConsumedSet() @@ -402,31 +399,26 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: /** Report a use/definition failure, where a previously hidden capability is * used again. - * @param tree the tree where the capability is used - * @param used the footprint of all uses of `tree` - * @param globalOverlap the overlap between `used` and all capabilities hidden - * by previous definitions + * @param tree the tree where the capability is used + * @param clashing the tree where the capability is previously hidden, + * or emptyTree if none exists + * @param used the uses of `tree` + * @param hidden the hidden set of the clashing def, + * or the global hidden set if no clashing def exists */ - def sepUseError(tree: Tree, used: Refs, globalOverlap: Refs)(using Context): Unit = - val individualChecks = for mdefs <- previousDefs.iterator; mdef <- mdefs.iterator yield - val hiddenByDef = captures(mdef.tpt).hiddenSet.footprint - val overlap = defUseOverlap(hiddenByDef, used, tree.symbol) - if !overlap.isEmpty then - def resultStr = if mdef.isInstanceOf[DefDef] then " result" else "" - report.error( - em"""Separation failure: Illegal access to ${CaptureSet(overlap)} which is hidden by the previous definition - |of ${mdef.symbol} with$resultStr type ${mdef.tpt.nuType}. - |This type hides capabilities ${CaptureSet(hiddenByDef)}""", - tree.srcPos) - true - else false - val clashes = individualChecks.filter(identity) - if clashes.hasNext then clashes.next // issues error as a side effect - else report.error( - em"""Separation failure: Illegal access to ${CaptureSet(globalOverlap)} which is hidden by some previous definitions - |No clashing definitions were found. This might point to an internal error.""", - tree.srcPos) - end sepUseError + def sepUseError(tree: Tree, clashingDef: ValOrDefDef | Null, used: Refs, hidden: Refs)(using Context): Unit = + if clashingDef != null then + def resultStr = if clashingDef.isInstanceOf[DefDef] then " result" else "" + report.error( + em"""Separation failure: Illegal access to ${overlapStr(hidden, used)} which is hidden by the previous definition + |of ${clashingDef.symbol} with$resultStr type ${clashingDef.tpt.nuType}. + |This type hides capabilities ${CaptureSet(hidden)}""", + tree.srcPos) + else + report.error( + em"""Separation failure: illegal access to ${overlapStr(hidden, used)} which is hidden by some previous definitions + |No clashing definitions were found. This might point to an internal error.""", + tree.srcPos) /** Report a failure where a previously consumed capability is used again, * @param ref the capability that is used after being consumed @@ -531,33 +523,37 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: currentPeaks.hidden ++ argPeaks.hidden) end checkApply - /** The def/use overlap between the references `hiddenByDef` hidden by - * a previous definition and the `used` set of a tree with symbol `sym`. - * Deduct any capabilities referred to or hidden by the (result-) type of `sym`. - */ - def defUseOverlap(hiddenByDef: Refs, used: Refs, sym: Symbol)(using Context): Refs = - val overlap = hiddenByDef.overlapWith(used) - resultType.get(sym) match - case Some(tp) if !overlap.isEmpty => - val declared = tp.captureSet.elems - overlap.deduct(declared.footprint).deduct(declared.hiddenSet.footprint) - case _ => - overlap - /** 1. Check that the capabilities used at `tree` don't overlap with * capabilities hidden by a previous definition. * 2. Also check that none of the used capabilities was consumed before. */ - def checkUse(tree: Tree)(using Context) = - val used = tree.markedFree - if !used.elems.isEmpty then - val usedFootprint = used.elems.footprint - val overlap = defUseOverlap(defsShadow, usedFootprint, tree.symbol) - if !overlap.isEmpty then - sepUseError(tree, usedFootprint, overlap) - for ref <- used.elems do + def checkUse(tree: Tree)(using Context): Unit = + val used = tree.markedFree.elems + if !used.isEmpty then + val usedPeaks = used.peaks + val overlap = defsShadow.peaks.sharedWith(usedPeaks) + if !defsShadow.peaks.sharedWith(usedPeaks).isEmpty then + val sym = tree.symbol + + def findClashing(prevDefs: List[DefInfo]): Option[DefInfo] = prevDefs match + case prevDef :: prevDefs1 => + if prevDef.symbol == sym then Some(prevDef) + else if !prevDef.hiddenPeaks.sharedWith(usedPeaks).isEmpty then Some(prevDef) + else findClashing(prevDefs1) + case Nil => + None + + findClashing(previousDefs) match + case Some(clashing) => + if clashing.symbol != sym then + sepUseError(tree, clashing.tree, used, clashing.hidden) + case None => + sepUseError(tree, null, used, defsShadow) + + for ref <- used do val pos = consumed.get(ref) if pos != null then consumeError(ref, pos, tree.srcPos) + end checkUse /** If `tp` denotes some version of a singleton type `x.type` the set `{x}` * otherwise the empty set. @@ -840,6 +836,10 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case tree: Apply => tree.symbol == defn.Caps_unsafeAssumeSeparate case _ => false + def pushDef(tree: ValOrDefDef, hiddenByDef: Refs)(using Context): Unit = + defsShadow ++= hiddenByDef + previousDefs = DefInfo(tree, tree.symbol, hiddenByDef, hiddenByDef.peaks) :: previousDefs + /** Check (result-) type of `tree` for separation conditions using `checkType`. * Excluded are parameters and definitions that have an =unsafeAssumeSeparate * application as right hand sides. @@ -848,11 +848,18 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def checkValOrDefDef(tree: ValOrDefDef)(using Context): Unit = if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then checkType(tree.tpt, tree.symbol) - if previousDefs.nonEmpty then - capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hiddenSet.footprint}") - defsShadow ++= captures(tree.tpt).hiddenSet.deductSymRefs(tree.symbol).footprint - resultType(tree.symbol) = tree.tpt.nuType - previousDefs.head += tree + capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hiddenSet.footprint}") + pushDef(tree, captures(tree.tpt).hiddenSet.deductSymRefs(tree.symbol)) + + def inSection[T](op: => T)(using Context): T = + val savedDefsShadow = defsShadow + val savedPrevionsDefs = previousDefs + try op + finally + previousDefs = savedPrevionsDefs + defsShadow = savedDefsShadow + + def traverseSection[T](tree: Tree)(using Context) = inSection(traverseChildren(tree)) /** Traverse `tree` and perform separation checks everywhere */ def traverse(tree: Tree)(using Context): Unit = @@ -870,19 +877,17 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: tree.tpe match case _: MethodOrPoly => case _ => traverseApply(tree, Nil) - case tree: Block => - val saved = defsShadow - previousDefs = mutable.ListBuffer() :: previousDefs - try traverseChildren(tree) - finally - previousDefs = previousDefs.tail - defsShadow = saved + case _: Block | _: Template => + traverseSection(tree) case tree: ValDef => traverseChildren(tree) checkValOrDefDef(tree) case tree: DefDef => - withFreshConsumed: - traverseChildren(tree) + inSection: + withFreshConsumed: + for params <- tree.paramss; case param: ValDef <- params do + pushDef(param, emptyRefs) + traverseChildren(tree) checkValOrDefDef(tree) case If(cond, thenp, elsep) => traverse(cond) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index b70c2b3e8940..ca7c7118db43 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -114,7 +114,7 @@ import annotation.{experimental, compileTimeOnly, retainsCap} /** A wrapper around code for which separation checks are suppressed. */ - def unsafeAssumeSeparate[T](op: T): T = op + def unsafeAssumeSeparate(op: Any): op.type = op end unsafe end caps \ No newline at end of file diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index f12576033622..10048255918e 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -463,8 +463,11 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz * $preservesLaziness */ override def partitionMap[A1, A2](f: A => Either[A1, A2]): (LazyListIterable[A1]^{this, f}, LazyListIterable[A2]^{this, f}) = { - val (left, right) = map(f).partition(_.isLeft) - (left.map(_.asInstanceOf[Left[A1, _]].value), right.map(_.asInstanceOf[Right[_, A2]].value)) + unsafeAssumeSeparate: + val part = map(f).partition(_.isLeft) + val left = part._1 + val right = part._2 + (left.map(_.asInstanceOf[Left[A1, _]].value), right.map(_.asInstanceOf[Right[_, A2]].value)) } /** @inheritdoc @@ -675,7 +678,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz override def dropRight(n: Int): LazyListIterable[A]^{this} = { if (n <= 0) this else if (knownIsEmpty) LazyListIterable.empty - else newLL { + else unsafeAssumeSeparate { newLL { var scout = this var remaining = n // advance scout n elements ahead (or until empty) @@ -683,9 +686,8 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz remaining -= 1 scout = scout.tail } - unsafeAssumeSeparate: - dropRightState(scout) - } + dropRightState(scout) + }} } private def dropRightState(scout: LazyListIterable[_]^): State[A]^{this, scout} = diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 22c4680a48fe..8db8791d1123 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -26,27 +26,45 @@ | Required: LazyRef[Int]^{cap1} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazyref.scala:8:24 ------------------------------------------------------------ +8 | new LazyRef(() => f(elem())) // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {LazyRef.this.elem} which is hidden by the previous definition + | of value get with type () => T. + | This type hides capabilities {LazyRef.this.elem} +-- Error: tests/neg-custom-args/captures/lazyref.scala:23:13 ----------------------------------------------------------- +23 | val ref3 = ref1.map(g) // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap1} which is hidden by the previous definition + | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {cap1} -- Error: tests/neg-custom-args/captures/lazyref.scala:26:9 ------------------------------------------------------------ 26 | if cap1 == cap2 // error: separation failure // error: separation failure | ^^^^ | Separation failure: Illegal access to {cap1} which is hidden by the previous definition - | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. - | This type hides capabilities {ref2*, cap1, cap2, ref1} + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} -- Error: tests/neg-custom-args/captures/lazyref.scala:26:17 ----------------------------------------------------------- 26 | if cap1 == cap2 // error: separation failure // error: separation failure | ^^^^ | Separation failure: Illegal access to {cap2} which is hidden by the previous definition - | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. - | This type hides capabilities {ref2*, cap1, cap2, ref1} + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} -- Error: tests/neg-custom-args/captures/lazyref.scala:27:11 ----------------------------------------------------------- -27 | then ref1 // error: separation failure +27 | then ref1 // error: separation failure | ^^^^ - | Separation failure: Illegal access to {ref1} which is hidden by the previous definition - | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. - | This type hides capabilities {ref2*, cap1, cap2, ref1} + | Separation failure: Illegal access to {cap1, ref1} which is hidden by the previous definition + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:28:11 ----------------------------------------------------------- +28 | else ref2) // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap1, cap2, ref1} which is hidden by the previous definition + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} -- Error: tests/neg-custom-args/captures/lazyref.scala:29:9 ------------------------------------------------------------ 29 | .map(g) // error: separation failure | ^ | Separation failure: Illegal access to {cap2} which is hidden by the previous definition - | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. - | This type hides capabilities {ref2*, cap1, cap2, ref1} + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 8715188f0ba1..396d9470ea17 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -5,7 +5,7 @@ type Cap = CC^ class LazyRef[T](val elem: () => T): val get: () => T = elem def map[U](f: T => U): LazyRef[U]^{f, this} = - new LazyRef(() => f(elem())) + new LazyRef(() => f(elem())) // error: separation failure def map[A, B](ref: LazyRef[A]^, f: A => B): LazyRef[B]^{f, ref} = new LazyRef(() => f(ref.elem())) @@ -20,11 +20,11 @@ def test(cap1: Cap, cap2: Cap) = val ref1c: LazyRef[Int] = ref1 // error val ref2 = map(ref1, g) val ref2c: LazyRef[Int]^{cap2} = ref2 // error - val ref3 = ref1.map(g) + val ref3 = ref1.map(g) // error: separation failure val ref3c: LazyRef[Int]^{ref1} = ref3 // error val ref4 = ( if cap1 == cap2 // error: separation failure // error: separation failure - then ref1 // error: separation failure - else ref2) + then ref1 // error: separation failure + else ref2) // error: separation failure .map(g) // error: separation failure val ref4c: LazyRef[Int]^{cap1} = ref4 // error diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check index 8f66aa5b3f42..3dd0306bfe0e 100644 --- a/tests/neg-custom-args/captures/sepchecks2.check +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -3,7 +3,7 @@ | ^ | Separation failure: Illegal access to {c} which is hidden by the previous definition | of value xs with type List[box () => Unit]. - | This type hides capabilities {xs*, c} + | This type hides capabilities {c} -- Error: tests/neg-custom-args/captures/sepchecks2.scala:13:7 --------------------------------------------------------- 13 | foo((() => println(c)) :: Nil, c) // error | ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/pos-custom-args/captures/path-use.scala b/tests/pos-custom-args/captures/path-use.scala index e738b81ae970..49268a449567 100644 --- a/tests/pos-custom-args/captures/path-use.scala +++ b/tests/pos-custom-args/captures/path-use.scala @@ -2,15 +2,15 @@ import language.experimental.namedTuples class IO -class C(val f: IO^): +class C(val ff: IO^): val procs: List[Proc] = ??? type Proc = () => Unit def test(io: IO^) = val c = C(io) - val f = () => println(c.f) - val _: () ->{c.f} Unit = f + val f = () => println(c.ff) + val _: () ->{c.ff} Unit = f val x = c.procs val _: List[() ->{c.procs*} Unit] = x From 6bb7cdc6961e0edec286966b8bf6d19feea53d4b Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 16 Feb 2025 10:43:11 +0100 Subject: [PATCH 40/93] More tests --- tests/neg-custom-args/captures/sep-box.check | 14 +++++++ tests/neg-custom-args/captures/sep-box.scala | 41 +++++++++++++++++++ tests/neg-custom-args/captures/sep-list.check | 14 +++++++ tests/neg-custom-args/captures/sep-list.scala | 39 ++++++++++++++++++ .../pos-custom-args/captures/sep-pairs.scala | 21 ++++++++++ 5 files changed, 129 insertions(+) create mode 100644 tests/neg-custom-args/captures/sep-box.check create mode 100644 tests/neg-custom-args/captures/sep-box.scala create mode 100644 tests/neg-custom-args/captures/sep-list.check create mode 100644 tests/neg-custom-args/captures/sep-list.scala create mode 100644 tests/pos-custom-args/captures/sep-pairs.scala diff --git a/tests/neg-custom-args/captures/sep-box.check b/tests/neg-custom-args/captures/sep-box.check new file mode 100644 index 000000000000..2a2608134130 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-box.check @@ -0,0 +1,14 @@ +-- Error: tests/neg-custom-args/captures/sep-box.scala:41:9 ------------------------------------------------------------ +41 | par(h1.value, h2.value) // error + | ^^^^^^^^ + | Separation failure: argument of type Ref^{xs*} + | to method par: (x: Ref^, y: Ref^): Unit + | corresponds to capture-polymorphic formal parameter x of type Ref^ + | and hides capabilities {xs*}. + | Some of these overlap with the captures of the second argument with type Ref^{xs*}. + | + | Hidden set of current argument : {xs*} + | Hidden footprint of current argument : {xs*} + | Capture set of second argument : {xs*} + | Footprint set of second argument : {xs*} + | The two sets overlap at : {xs*} diff --git a/tests/neg-custom-args/captures/sep-box.scala b/tests/neg-custom-args/captures/sep-box.scala new file mode 100644 index 000000000000..fd9348acc893 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-box.scala @@ -0,0 +1,41 @@ +import caps.Mutable +import caps.cap + +abstract class LIST[+T]: + def isEmpty: Boolean + def head: T + def tail: LIST[T] + def map[U](f: T => U): LIST[U] = + if isEmpty then NIL + else CONS(f(head), tail.map(f)) + +class CONS[+T](x: T, xs: LIST[T]) extends LIST[T]: + def isEmpty = false + def head = x + def tail = xs +object NIL extends LIST[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +class Box[+X](val value: X) + +def listFresh(n: Int): LIST[Box[Ref^]] = + if n == 0 then NIL + else + val hd = Ref() + val tl = listFresh(n - 1) + CONS(Box(hd), tl) + +def par(x: Ref^, y: Ref^): Unit = () + +def test = + val xs = listFresh(10) + val h1 = xs.head + val h2 = xs.head + par(h1.value, h2.value) // error diff --git a/tests/neg-custom-args/captures/sep-list.check b/tests/neg-custom-args/captures/sep-list.check new file mode 100644 index 000000000000..86d4937677e8 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-list.check @@ -0,0 +1,14 @@ +-- Error: tests/neg-custom-args/captures/sep-list.scala:39:6 ----------------------------------------------------------- +39 | par(h1, h2) // error + | ^^ + | Separation failure: argument of type (h1 : Ref^{xs*}) + | to method par: (x: Ref^, y: Ref^): Unit + | corresponds to capture-polymorphic formal parameter x of type Ref^ + | and hides capabilities {h1}. + | Some of these overlap with the captures of the second argument with type (h2 : Ref^{xs*}). + | + | Hidden set of current argument : {h1} + | Hidden footprint of current argument : {h1, xs*} + | Capture set of second argument : {h2} + | Footprint set of second argument : {h2, xs*} + | The two sets overlap at : {xs*} diff --git a/tests/neg-custom-args/captures/sep-list.scala b/tests/neg-custom-args/captures/sep-list.scala new file mode 100644 index 000000000000..46e9b6988318 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-list.scala @@ -0,0 +1,39 @@ +import caps.Mutable +import caps.cap + +abstract class LIST[+T]: + def isEmpty: Boolean + def head: T + def tail: LIST[T] + def map[U](f: T => U): LIST[U] = + if isEmpty then NIL + else CONS(f(head), tail.map(f)) + +class CONS[+T](x: T, xs: LIST[T]) extends LIST[T]: + def isEmpty = false + def head = x + def tail = xs +object NIL extends LIST[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +def listFresh(n: Int): LIST[Ref^] = + if n == 0 then NIL + else + val hd = Ref() + val tl = listFresh(n - 1) + CONS(hd, tl) + +def par(x: Ref^, y: Ref^): Unit = () + +def test = + val xs = listFresh(10) + val h1 = xs.head + val h2 = xs.head + par(h1, h2) // error diff --git a/tests/pos-custom-args/captures/sep-pairs.scala b/tests/pos-custom-args/captures/sep-pairs.scala new file mode 100644 index 000000000000..df1a3098d22d --- /dev/null +++ b/tests/pos-custom-args/captures/sep-pairs.scala @@ -0,0 +1,21 @@ +import caps.Mutable +import caps.{cap, consume, use} + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +case class Pair[+A, +B](fst: A, snd: B) + +def mkPair: Pair[Ref^, Ref^] = + val r1 = Ref() + val r2 = Ref() + val p_exact: Pair[Ref^{r1}, Ref^{r2}] = Pair(r1, r2) + p_exact + +def copyPair(@consume @use p: Pair[Ref^, Ref^]): Pair[Ref^, Ref^] = + val x: Ref^{p.fst*} = p.fst + val y: Ref^{p.snd*} = p.snd + Pair(x, y) + From a870efff9e25755e387efc0d865d1c8f07009981 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 19 Feb 2025 08:31:59 +0100 Subject: [PATCH 41/93] Fix: Don't add implicit Capability captures to result types of constructors --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 14 +++++++++----- tests/neg-custom-args/captures/sep-pairs.check | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 56aeac274164..d2bf15dbd313 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 Fresh.fromCap(transformExplicitType(symd.info), sym) + else Fresh.fromCap(transformExplicitType(symd.info, sym), sym) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) else if isPreCC(sym) then @@ -302,7 +302,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 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 = + private def transformExplicitType(tp: Type, sym: Symbol, tptToCheck: Tree = EmptyTree)(using Context): Type = def fail(msg: Message) = if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) @@ -387,7 +387,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: this(expandThrowsAlias(res, exc, Nil)) case t => // Map references to capability classes C to C^ - if t.derivesFromCapability && !t.isSingleton && t.typeSymbol != defn.Caps_Exists + if t.derivesFromCapability + && !t.isSingleton + && t.typeSymbol != defn.Caps_Exists + && (!sym.isConstructor || (t ne tp.finalResultType)) + // Don't add ^ to result types of class constructors deriving from Capability then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) else normalizeCaptures(mapFollowingAliases(t)) end toCapturing @@ -460,7 +464,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: var transformed = if tree.isInferred then transformInferredType(tree.tpe) - else transformExplicitType(tree.tpe, tptToCheck = tree) + else transformExplicitType(tree.tpe, sym, tptToCheck = tree) if boxed then transformed = box(transformed) if sym.is(Param) && (transformed ne tree.tpe) then paramSigChange += tree @@ -685,7 +689,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // Compute new parent types val ps1 = inContext(ctx.withOwner(cls)): - ps.mapConserve(transformExplicitType(_)) + ps.mapConserve(transformExplicitType(_, NoSymbol)) // Install new types and if it is a module class also update module object if (selfInfo1 ne selfInfo) || (ps1 ne ps) then diff --git a/tests/neg-custom-args/captures/sep-pairs.check b/tests/neg-custom-args/captures/sep-pairs.check index 948c822ce5ac..128474d7b303 100644 --- a/tests/neg-custom-args/captures/sep-pairs.check +++ b/tests/neg-custom-args/captures/sep-pairs.check @@ -1,20 +1,20 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sep-pairs.scala:32:10 ------------------------------------ 32 | Pair(Ref(), Ref()) // error // error: universal capability cannot be included in capture set | ^^^^^ - | Found: box Ref^{cap.rd, cap} + | Found: box Ref^ | Required: box Ref^? | - | Note that the universal capability `cap.rd` + | Note that the universal capability `cap` | cannot be included in capture set ? | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sep-pairs.scala:32:17 ------------------------------------ 32 | Pair(Ref(), Ref()) // error // error: universal capability cannot be included in capture set | ^^^^^ - | Found: box Ref^{cap.rd, cap} + | Found: box Ref^ | Required: box Ref^? | - | Note that the universal capability `cap.rd` + | Note that the universal capability `cap` | cannot be included in capture set ? | | longer explanation available when compiling with `-explain` From 117d5e314a72e66e481ba72030448bdfd883bed4 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 19 Feb 2025 12:31:32 +0100 Subject: [PATCH 42/93] Dont apply Fresh.FromCap to inferred types They are already inferred with Fresh.Cap instances, so adding another map just confuses things. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 13 +++++++-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 28 ++++++++++++++++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 1 + .../neg-custom-args/captures/outer-var.check | 2 +- tests/neg-custom-args/captures/vars.check | 7 ++--- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index cb638d9cfa95..7685cc2f58f9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -480,7 +480,9 @@ object CaptureSet: end Fluid /** The subclass of captureset variables with given initial elements */ - class Var(override val owner: Symbol = NoSymbol, initialElems: Refs = emptyRefs, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: + class Var(initialOwner: Symbol = NoSymbol, initialElems: Refs = emptyRefs, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: + + override def owner = initialOwner /** A unique identification number for diagnostics */ val id = @@ -951,9 +953,14 @@ object CaptureSet: * which are already subject through snapshotting and rollbacks in VarState. * It's advantageous if we don't need to deal with other pieces of state there. */ - class HiddenSet(owner: Symbol, initialHidden: Refs = emptyRefs)(using @constructorOnly ictx: Context) - extends Var(owner, initialHidden): + class HiddenSet(initialOwner: Symbol, initialHidden: Refs = emptyRefs)(using @constructorOnly ictx: Context) + extends Var(initialOwner, initialHidden): var owningCap: AnnotatedType = uninitialized + var givenOwner: Symbol = initialOwner + + override def owner = givenOwner + + // assert(id != 34, i"$initialHidden") private def aliasRef: AnnotatedType | Null = if myElems.size == 1 then diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 9b7dc0455a9c..5f7277dc14df 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -326,13 +326,33 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) + /* Also set any previously unset owners of toplevel Fresh.Cap instances to improve + * error diagnostics in separation checking. + */ + private def anchorCaps(sym: Symbol)(using Context) = new TypeTraverser: + override def traverse(t: Type) = + if variance > 0 then + t match + case t @ CapturingType(parent, refs) => + for ref <- refs.elems do + ref match + case Fresh.Cap(hidden) if !hidden.givenOwner.exists => + hidden.givenOwner = sym + case _ => + traverse(parent) + case t @ defn.RefinedFunctionOf(rinfo) => + traverse(rinfo) + case _ => + traverseChildren(t) + /** If `tpt` is an inferred type, interpolate capture set variables appearing contra- - * variantly in it. + * variantly in it. Also anchor Fresh.Cap instances with anchorCaps. */ - private def interpolateVarsIn(tpt: Tree)(using Context): Unit = + private def interpolateVarsIn(tpt: Tree, sym: Symbol)(using Context): Unit = if tpt.isInstanceOf[InferredTypeTree] then interpolator().traverse(tpt.nuType) .showing(i"solved vars in ${tpt.nuType}", capt) + anchorCaps(sym).traverse(tpt.nuType) for msg <- ccState.approxWarnings do report.warning(msg, tpt.srcPos) ccState.approxWarnings.clear() @@ -952,7 +972,7 @@ class CheckCaptures extends Recheck, SymTransformer: // for more info from the context, so we cannot interpolate. Note that we cannot // expect to have all necessary info available at the point where the anonymous // function is compiled since we do not propagate expected types into blocks. - interpolateVarsIn(tree.tpt) + interpolateVarsIn(tree.tpt, sym) /** Recheck method definitions: * - check body in a nested environment that tracks uses, in a nested level, @@ -998,7 +1018,7 @@ class CheckCaptures extends Recheck, SymTransformer: if !sym.isAnonymousFunction then // Anonymous functions propagate their type to the enclosing environment // so it is not in general sound to interpolate their types. - interpolateVarsIn(tree.tpt) + interpolateVarsIn(tree.tpt, sym) curEnv = saved end recheckDefDef diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index d2bf15dbd313..84d1f5068b1a 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -471,6 +471,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tree.setNuType( if boxed then transformed else if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) + else if tree.isInferred then transformed else Fresh.fromCap(transformed, sym)) /** Transform the type of a val or var or the result type of a def */ diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index 0c86213ff118..a55abfaaf98d 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 : () => Unit), defined in method inner - | cannot be included in outer capture set {p} + | cannot be included in outer capture set {p} of variable y | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/outer-var.scala:17:57 --------------------------------------------------------- diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 4fe4163aa433..db5c8083e3b7 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,11 +1,10 @@ -- Error: tests/neg-custom-args/captures/vars.scala:24:14 -------------------------------------------------------------- 24 | a = x => g(x) // error | ^^^^ - | reference (cap3 : CC^) is not included in the allowed capture set {cap1} - | of an enclosing function literal with expected type (x$0: String) ->{cap1} String + | reference (cap3 : CC^) is not included in the allowed capture set {cap1} of variable a | | Note that reference (cap3 : CC^), defined in method scope - | cannot be included in outer capture set {cap1} + | cannot be included in outer capture set {cap1} of variable a -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:8 ------------------------------------------ 25 | a = g // error | ^ @@ -13,7 +12,7 @@ | Required: (x$0: String) ->{cap1} String | | Note that reference (cap3 : CC^), defined in method scope - | cannot be included in outer capture set {cap1} + | cannot be included in outer capture set {cap1} of variable a | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:27:12 ----------------------------------------- From 72bf9c6b88026e869ff7a15e549296b7463164a0 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 19 Feb 2025 17:06:40 +0100 Subject: [PATCH 43/93] Also include nested caps when computing hidden members of a capture set --- .../src/dotty/tools/dotc/cc/SepCheck.scala | 58 ++++++++----------- .../captures/sep-counter.check | 7 +++ .../captures/sep-counter.scala | 16 +++++ .../neg-custom-args/captures/sep-pairs.check | 4 +- 4 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 tests/neg-custom-args/captures/sep-counter.check create mode 100644 tests/neg-custom-args/captures/sep-counter.scala diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index acb26233dcae..b378ba02b193 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -180,18 +180,19 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: extension (refs: Refs) /** The footprint of a set of references `refs` the smallest set `F` such that - * - no maximal capability is in `F` - * - all non-maximal capabilities in `refs` are in `F` - * - if `f in F` then the footprint of `f`'s info is also in `F`. + * 1. if includeMax is false then no maximal capability is in `F` + * 2. all capabilities in `refs` satisfying (1) are in `F` + * 3. if `f in F` then the footprint of `f`'s info is also in `F`. */ - private def footprint(using Context): Refs = + private def footprint(includeMax: Boolean = false)(using Context): Refs = + def retain(ref: CaptureRef) = includeMax || !ref.isMaxCapability def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match case newElem :: newElems1 => val superElems = newElem.captureSetOfInfo.elems.filter: superElem => - !superElem.isMaxCapability && !elems.contains(superElem) + retain(superElem) && !elems.contains(superElem) recur(elems ++ superElems, newElems1 ++ superElems.toList) case Nil => elems - val elems: Refs = refs.filter(!_.isMaxCapability) + val elems: Refs = refs.filter(retain) recur(elems, elems.toList) private def peaks(using Context): Refs = @@ -269,7 +270,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet def hiddenByElem(elem: CaptureRef): Refs = elem match - case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) + case Fresh.Cap(hcs) => hcs.elems ++ recur(hcs.elems) case ReadOnlyCapability(ref1) => hiddenByElem(ref1).map(_.readOnly) case _ => emptyRefs @@ -280,20 +281,6 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: recur(refs) end hiddenSet - /** Same as !refs.hidden.isEmpty but more efficient */ - private def containsHidden(using Context): Boolean = - val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet - - def recur(refs: Refs): Boolean = refs.exists: ref => - seen.add(ref) && ref.stripReadOnly.match - case Fresh.Cap(hcs) => - hcs.elems.exists(!_.isRootCapability) || recur(hcs.elems) - case _ => - false - - recur(refs) - end containsHidden - /** Subtract all elements that are covered by some element in `others` from this set. */ private def deduct(others: Refs)(using Context): Refs = refs.filter: ref => @@ -302,7 +289,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: /** Deduct the footprint of `sym` and `sym*` from `refs` */ private def deductSymFootprint(sym: Symbol)(using Context): Refs = val ref = sym.termRef - if ref.isTrackableRef then refs.deduct(CaptureSet(ref, ref.reach).elems.footprint) + if ref.isTrackableRef then refs.deduct(CaptureSet(ref, ref.reach).elems.footprint()) else refs /** Deduct `sym` and `sym*` from `refs` */ @@ -314,7 +301,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: /** Deduct the footprint of all captures of trees in `deps` from `refs` */ private def deductCapturesOf(deps: List[Tree])(using Context): Refs = deps.foldLeft(refs): (refs, dep) => - refs.deduct(captures(dep).footprint) + refs.deduct(captures(dep).footprint()) end extension /** The deep capture set of an argument or prefix widened to the formal parameter, if @@ -333,16 +320,19 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: shared.nth(0) match case fresh @ Fresh.Cap(hidden) => if hidden.owner.exists then i"$fresh of ${hidden.owner}" else i"$fresh" + case other => + i"$other" def overlapStr(hiddenSet: Refs, clashSet: Refs)(using Context): String = - val hiddenFootprint = hiddenSet.footprint - val clashFootprint = clashSet.footprint + val hiddenFootprint = hiddenSet.footprint() + val clashFootprint = clashSet.footprint() // The overlap of footprints, or, of this empty the set of shared peaks. // We prefer footprint overlap since it tends to be more informative. val overlap = hiddenFootprint.overlapWith(clashFootprint) if !overlap.isEmpty then i"${CaptureSet(overlap)}" else - val sharedPeaks = hiddenSet.peaks.sharedWith(clashSet.peaks) + val sharedPeaks = hiddenSet.footprint(includeMax = true).sharedWith: + clashSet.footprint(includeMax = true) assert(!sharedPeaks.isEmpty, i"no overlap for $hiddenSet vs $clashSet") sharedPeaksStr(sharedPeaks) @@ -391,9 +381,9 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: |Some of these overlap with the captures of the ${clashArgStr.trim}$clashTypeStr. | | Hidden set of current argument : ${CaptureSet(hiddenSet)} - | Hidden footprint of current argument : ${CaptureSet(hiddenSet.footprint)} + | Hidden footprint of current argument : ${CaptureSet(hiddenSet.footprint())} | Capture set of $clashArgStr : ${CaptureSet(clashSet)} - | Footprint set of $clashArgStr : ${CaptureSet(clashSet.footprint)} + | Footprint set of $clashArgStr : ${CaptureSet(clashSet.footprint())} | The two sets overlap at : ${overlapStr(hiddenSet, clashSet)}""", polyArg.srcPos) @@ -657,7 +647,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val captured = genPart.deepCaptureSet.elems val hiddenSet = captured.hiddenSet.pruned val clashSet = otherPart.deepCaptureSet.elems - val deepClashSet = (clashSet.footprint ++ clashSet.hiddenSet).pruned + val deepClashSet = (clashSet.footprint() ++ clashSet.hiddenSet).pruned report.error( em"""Separation failure in ${role.description} $tpe. |One part, $genPart, hides capabilities ${CaptureSet(hiddenSet)}. @@ -735,7 +725,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: c.add(c1) case t @ CapturingType(parent, cs) => val c1 = this(c, parent) - if cs.elems.containsHidden then c1.add(Captures.Hidden) + if cs.elems.exists(_.stripReadOnly.isFresh) then c1.add(Captures.Hidden) else if !cs.elems.isEmpty then c1.add(Captures.Explicit) else c1 case t: TypeRef if t.symbol.isAbstractOrParamType => @@ -760,11 +750,11 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: // "see through them" when we look at hidden sets. then val refs = tpe.deepCaptureSet.elems - val toCheck = refs.hiddenSet.footprint.deduct(refs.footprint) + val toCheck = refs.hiddenSet.footprint().deduct(refs.footprint()) checkConsumedRefs(toCheck, tpe, role, i"${role.description} $tpe hides", pos) case TypeRole.Argument(arg) => if tpe.hasAnnotation(defn.ConsumeAnnot) then - val capts = captures(arg).footprint + val capts = captures(arg).footprint() checkConsumedRefs(capts, tpe, role, i"argument to @consume parameter with type ${arg.nuType} refers to", pos) case _ => @@ -848,7 +838,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def checkValOrDefDef(tree: ValOrDefDef)(using Context): Unit = if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then checkType(tree.tpt, tree.symbol) - capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hiddenSet.footprint}") + capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hiddenSet.footprint()}") pushDef(tree, captures(tree.tpt).hiddenSet.deductSymRefs(tree.symbol)) def inSection[T](op: => T)(using Context): T = @@ -869,7 +859,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case tree @ Select(qual, _) if tree.symbol.is(Method) && tree.symbol.hasAnnotation(defn.ConsumeAnnot) => traverseChildren(tree) checkConsumedRefs( - captures(qual).footprint, qual.nuType, + captures(qual).footprint(), qual.nuType, TypeRole.Qualifier(qual, tree.symbol), i"call prefix of @consume ${tree.symbol} refers to", qual.srcPos) case tree: GenericApply => diff --git a/tests/neg-custom-args/captures/sep-counter.check b/tests/neg-custom-args/captures/sep-counter.check new file mode 100644 index 000000000000..230c1e6f6e53 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-counter.check @@ -0,0 +1,7 @@ +-- Error: tests/neg-custom-args/captures/sep-counter.scala:12:19 ------------------------------------------------------- +12 | def mkCounter(): Pair[Ref^, Ref^] = // error + | ^^^^^^^^^^^^^^^^ + | Separation failure in method mkCounter's result type Pair[box Ref^, box Ref^]. + | One part, box Ref^, hides capabilities {cap}. + | Another part, box Ref^, captures capabilities {cap}. + | The two sets overlap at cap of value c. diff --git a/tests/neg-custom-args/captures/sep-counter.scala b/tests/neg-custom-args/captures/sep-counter.scala new file mode 100644 index 000000000000..42bf5afc172a --- /dev/null +++ b/tests/neg-custom-args/captures/sep-counter.scala @@ -0,0 +1,16 @@ +import caps.Mutable +import caps.cap + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +class Pair[+X, +Y](val fst: X, val snd: Y) + +def test() = + def mkCounter(): Pair[Ref^, Ref^] = // error + val c = Ref() + val p: Pair[Ref^{c}, Ref^{c}] = Pair(c, c) + //val q: Pair[Ref^, Ref^] = p + p diff --git a/tests/neg-custom-args/captures/sep-pairs.check b/tests/neg-custom-args/captures/sep-pairs.check index 128474d7b303..fd3e00c138bf 100644 --- a/tests/neg-custom-args/captures/sep-pairs.check +++ b/tests/neg-custom-args/captures/sep-pairs.check @@ -29,8 +29,8 @@ 13 |def bad: Pair[Ref^, Ref^] = // error: overlap at r1*, r0 | ^^^^^^^^^^^^^^^^ | Separation failure in method bad's result type Pair[box Ref^, box Ref^]. - | One part, box Ref^, hides capabilities {r1*, r0}. - | Another part, box Ref^, captures capabilities {r1*, r0}. + | One part, box Ref^, hides capabilities {cap, cap, r1*, r0}. + | Another part, box Ref^, captures capabilities {cap, cap, r1*, r0}. | The two sets overlap at {r1*, r0}. -- Error: tests/neg-custom-args/captures/sep-pairs.scala:44:18 --------------------------------------------------------- 44 | val sameToPair: Pair[Ref^, Ref^] = Pair(fstSame, sndSame) // error From 1430a206069d850755d211e436afce5d02a5a874 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 20 Feb 2025 19:08:59 +0100 Subject: [PATCH 44/93] Fix soundness problem with curried functions Flags the following as an error: ```scala val foo: (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit = x => y => swap(x, y) val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error f(a) ``` Here, the result type of `foo(a)` takes an argument with `a` capture but also refers to a hidden `a` in its `x` dependency. We now recognize and reject this case. ``` --- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 26 ++++- .../src/dotty/tools/dotc/cc/SepCheck.scala | 106 ++++++++++++------ .../captures/sep-curried.check | 70 ++++++++++++ .../captures/sep-curried.scala | 49 ++++++++ 5 files changed, 212 insertions(+), 41 deletions(-) create mode 100644 tests/neg-custom-args/captures/sep-curried.check create mode 100644 tests/neg-custom-args/captures/sep-curried.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index c9fde7323928..5c0f1aa2e496 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -259,7 +259,7 @@ trait CaptureRef extends TypeProxy, ValueType: vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) || !y.stripReadOnly.isCap && canAddHidden && vs.addHidden(hidden, y) case _ => - this.isCap && canAddHidden + this.isCap && canAddHidden && vs != VarState.HardSeparate || y.match case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 7685cc2f58f9..d07edf63af83 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -197,7 +197,7 @@ sealed abstract class CaptureSet extends Showable: // For instance x: C^{y, z}. Then neither y nor z subsumes x but {y, z} accounts for x. !x.isMaxCapability && !x.derivesFrom(defn.Caps_CapSet) - && !(vs == VarState.Separate && x.captureSetOfInfo.containsRootCapability) + && !(vs.isSeparating && x.captureSetOfInfo.containsRootCapability) // in VarState.Separate, don't try to widen to cap since that might succeed with {cap} <: {cap} && x.captureSetOfInfo.subCaptures(this, VarState.Separate).isOK @@ -257,9 +257,9 @@ sealed abstract class CaptureSet extends Showable: * `this` and `that` */ def ++ (that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, VarState.Separate).isOK then + if this.subCaptures(that, VarState.HardSeparate).isOK then if that.isAlwaysEmpty && this.keepAlways then this else that - else if that.subCaptures(this, VarState.Separate).isOK then this + else if that.subCaptures(this, VarState.HardSeparate).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) else Union(this, that) @@ -554,7 +554,7 @@ object CaptureSet: else // id == 108 then assert(false, i"trying to add $elem to $this") assert(elem.isTrackableRef, elem) - assert(!this.isInstanceOf[HiddenSet] || summon[VarState] == VarState.Separate, summon[VarState]) + assert(!this.isInstanceOf[HiddenSet] || summon[VarState].isSeparating, summon[VarState]) elems += elem if elem.isRootCapability then rootAddedHandler() @@ -1157,6 +1157,7 @@ object CaptureSet: /** Does this state allow additions of elements to capture set variables? */ def isOpen = true + def isSeparating = false /** Add element to hidden set, recording it in elemsMap, * return whether this was allowed. By default, recording is allowed @@ -1204,10 +1205,23 @@ object CaptureSet: * reference `r` only if `r` is already present in the hidden set of the instance. * No new references can be added. */ - @sharable - object Separate extends Closed: + class Separating extends Closed: override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = false override def toString = "separating varState" + override def isSeparating = true + + /** A closed state that allows a Fresh.Cap instance to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. + */ + @sharable + object Separate extends Separating + + /** Like Separate but in addition we assume that `cap` never subsumes anything else. + * Used in `++` to not lose track of dependencies between function parameters. + */ + @sharable + object HardSeparate extends Separating /** A special state that turns off recording of elements. Used only * in `addSub` to prevent cycles in recordings. diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index b378ba02b193..036d12b94f4a 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -340,11 +340,13 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * @param fn the function * @param parts the function prefix followed by the flattened argument list * @param polyArg the clashing argument to a polymorphic formal - * @param clashing the argument with which it clashes + * @param clashing the argument, function prefix, or entire function application result with + * which it clashes, + * */ def sepApplyError(fn: Tree, parts: List[Tree], polyArg: Tree, clashing: Tree)(using Context): Unit = val polyArgIdx = parts.indexOf(polyArg).ensuring(_ >= 0) - 1 - val clashIdx = parts.indexOf(clashing).ensuring(_ >= 0) + val clashIdx = parts.indexOf(clashing) // -1 means entire function application def paramName(mt: Type, idx: Int): Option[Name] = mt match case mt @ MethodType(pnames) => if idx < pnames.length then Some(pnames(idx)) else paramName(mt.resType, idx - pnames.length) @@ -363,11 +365,12 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if isShowableMethod then i"${fn.symbol}: ${fn.symbol.info}" else i"a function of type ${funType.widen}" def clashArgStr = clashIdx match - case 0 => "function prefix" - case 1 => "first argument " - case 2 => "second argument" - case 3 => "third argument " - case n => s"${n}th argument " + case -1 => "function result" + case 0 => "function prefix" + case 1 => "first argument " + case 2 => "second argument" + case 3 => "third argument " + case n => s"${n}th argument " def clashTypeStr = if clashIdx == 0 && !isShowableMethod then "" // we already mentioned the type in `funStr` else i" with type ${clashing.nuType}" @@ -455,11 +458,12 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * * @param fn the applied function * @param args the flattened argument lists + * @param app the entire application tree * @param deps cross argument dependencies: maps argument trees to * those other arguments that where mentioned by coorresponding * formal parameters. */ - private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = + private def checkApply(fn: Tree, args: List[Tree], app: Tree, deps: collection.Map[Tree, List[Tree]])(using Context): Unit = val (qual, fnCaptures) = methPart(fn) match case Select(qual, _) => (qual, qual.nuType.captureSet) case _ => (fn, CaptureSet.empty) @@ -511,6 +515,29 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: currentPeaks = PeaksPair( currentPeaks.actual ++ argPeaks.actual, currentPeaks.hidden ++ argPeaks.hidden) + end for + + def collectRefs(args: List[Type], res: Type) = + args.foldLeft(argCaptures(res)): (refs, arg) => + refs ++ arg.deepCaptureSet.elems + + /** The deep capture sets of all parameters of this type (if it is a function type) */ + def argCaptures(tpe: Type): Refs = tpe match + case defn.FunctionOf(args, resultType, isContextual) => + collectRefs(args, resultType) + case defn.RefinedFunctionOf(mt) => + collectRefs(mt.paramInfos, mt.resType) + case CapturingType(parent, _) => + argCaptures(parent) + case _ => + emptyRefs + + if !deps(app).isEmpty then + lazy val appPeaks = argCaptures(app.nuType).peaks + lazy val partPeaks = partsWithPeaks.toMap + for arg <- deps(app) do + if arg.needsSepCheck && !partPeaks(arg).hidden.sharedWith(appPeaks).isEmpty then + sepApplyError(fn, parts, arg, app) end checkApply /** 1. Check that the capabilities used at `tree` don't overlap with @@ -782,44 +809,55 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * * f(x: A, y: B^{cap, x}, z: C^{x, y}): D * - * then the dependencies of an application `f(a, b)` is a map that takes - * `b` to `List(a)` and `c` to `List(a, b)`. + * then the dependencies of an application `f(a, b, c)` of type C^{y} is the map + * + * [ b -> [a] + * , c -> [a, b] + * , f(a, b, c) -> [b]] */ - private def dependencies(fn: Tree, argss: List[List[Tree]])(using Context): collection.Map[Tree, List[Tree]] = + private def dependencies(fn: Tree, argss: List[List[Tree]], app: Tree)(using Context): collection.Map[Tree, List[Tree]] = + def isFunApply(sym: Symbol) = + sym.name == nme.apply && defn.isFunctionClass(sym.owner) val mtpe = - if fn.symbol.exists then fn.symbol.info - else fn.tpe.widen // happens for PolyFunction applies + if fn.symbol.exists && !isFunApply(fn.symbol) then fn.symbol.info + else fn.nuType.widen val mtps = collectMethodTypes(mtpe) assert(mtps.hasSameLengthAs(argss), i"diff for $fn: ${fn.symbol} /// $mtps /// $argss") val mtpsWithArgs = mtps.zip(argss) val argMap = mtpsWithArgs.toMap val deps = mutable.HashMap[Tree, List[Tree]]().withDefaultValue(Nil) - for - (mt, args) <- mtpsWithArgs - (formal, arg) <- mt.paramInfos.zip(args) - dep <- formal.captureSet.elems.toList - do - val referred = dep.stripReach match - case dep: TermParamRef => - argMap(dep.binder)(dep.paramNum) :: Nil - case dep: ThisType if dep.cls == fn.symbol.owner => - val Select(qual, _) = fn: @unchecked // TODO can we use fn instead? - qual :: Nil - case _ => - Nil - deps(arg) ++= referred + + def recordDeps(formal: Type, actual: Tree) = + for dep <- formal.captureSet.elems.toList do + val referred = dep.stripReach match + case dep: TermParamRef => + argMap(dep.binder)(dep.paramNum) :: Nil + case dep: ThisType if dep.cls == fn.symbol.owner => + val Select(qual, _) = fn: @unchecked // TODO can we use fn instead? + qual :: Nil + case _ => + Nil + deps(actual) ++= referred + + for (mt, args) <- mtpsWithArgs; (formal, arg) <- mt.paramInfos.zip(args) do + recordDeps(formal, arg) + recordDeps(mtpe.finalResultType, app) + capt.println(i"deps for $app = ${deps.toList}") deps + /** Decompose an application into a function prefix and a list of argument lists. * If some of the arguments need a separation check because they are capture polymorphic, * perform a separation check with `checkApply` */ - private def traverseApply(tree: Tree, argss: List[List[Tree]])(using Context): Unit = tree match - case Apply(fn, args) => traverseApply(fn, args :: argss) - case TypeApply(fn, args) => traverseApply(fn, argss) // skip type arguments - case _ => - if argss.nestedExists(_.needsSepCheck) then - checkApply(tree, argss.flatten, dependencies(tree, argss)) + private def traverseApply(app: Tree)(using Context): Unit = + def recur(tree: Tree, argss: List[List[Tree]]): Unit = tree match + case Apply(fn, args) => recur(fn, args :: argss) + case TypeApply(fn, args) => recur(fn, argss) // skip type arguments + case _ => + if argss.nestedExists(_.needsSepCheck) then + checkApply(tree, argss.flatten, app, dependencies(tree, argss, app)) + recur(app, Nil) /** Is `tree` an application of `caps.unsafe.unsafeAssumeSeparate`? */ def isUnsafeAssumeSeparate(tree: Tree)(using Context): Boolean = tree match @@ -866,7 +904,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: traverseChildren(tree) tree.tpe match case _: MethodOrPoly => - case _ => traverseApply(tree, Nil) + case _ => traverseApply(tree) case _: Block | _: Template => traverseSection(tree) case tree: ValDef => diff --git a/tests/neg-custom-args/captures/sep-curried.check b/tests/neg-custom-args/captures/sep-curried.check new file mode 100644 index 000000000000..313c5c8e461b --- /dev/null +++ b/tests/neg-custom-args/captures/sep-curried.check @@ -0,0 +1,70 @@ +-- Error: tests/neg-custom-args/captures/sep-curried.scala:16:6 -------------------------------------------------------- +16 | foo(a)(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to method foo: (x: Ref[Int]^)(y: Ref[Int]^{a}): Unit + | corresponds to capture-polymorphic formal parameter x of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the second argument with type (a : Ref[Int]^). + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of second argument : {a} + | Footprint set of second argument : {a} + | The two sets overlap at : {a} +-- Error: tests/neg-custom-args/captures/sep-curried.scala:22:44 ------------------------------------------------------- +22 | val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to a function of type (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit + | corresponds to capture-polymorphic formal parameter x of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the function result with type (y: Ref[Int]^{a}) ->{a} Unit. + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of function result : {a} + | Footprint set of function result : {a} + | The two sets overlap at : {a} +-- Error: tests/neg-custom-args/captures/sep-curried.scala:29:6 -------------------------------------------------------- +29 | foo(a)(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to a function of type (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit + | corresponds to capture-polymorphic formal parameter x of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the function result with type (y: Ref[Int]^{a}) ->{a} Unit. + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of function result : {a} + | Footprint set of function result : {a} + | The two sets overlap at : {a} +-- Error: tests/neg-custom-args/captures/sep-curried.scala:35:9 -------------------------------------------------------- +35 | foo(a)(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to a function of type (y: Ref[Int]^) ->{a} Unit + | corresponds to capture-polymorphic formal parameter y of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of function prefix : {a} + | Footprint set of function prefix : {a} + | The two sets overlap at : {a} +-- Error: tests/neg-custom-args/captures/sep-curried.scala:42:4 -------------------------------------------------------- +42 | f(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to a function of type (y: Ref[Int]^) ->{a} Unit + | corresponds to capture-polymorphic formal parameter y of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of function prefix : {f} + | Footprint set of function prefix : {f, a} + | The two sets overlap at : {a} diff --git a/tests/neg-custom-args/captures/sep-curried.scala b/tests/neg-custom-args/captures/sep-curried.scala new file mode 100644 index 000000000000..f8abc8a15086 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-curried.scala @@ -0,0 +1,49 @@ +import language.experimental.captureChecking +import caps.* + +class Ref[T](init: T) extends Mutable: + private var value: T = init + def get: T = value + mut def set(newValue: T): Unit = value = newValue + +// a library function that assumes that a and b MUST BE separate +def swap[T](a: Ref[Int]^, b: Ref[Int]^): Unit = ??? + +def test0(): Unit = + val a: Ref[Int]^ = Ref(0) + def foo(x: Ref[Int]^)(y: Ref[Int]^{a}): Unit = + swap(x, y) + foo(a)(a) // error + +def test1(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit = + x => y => swap(x, y) + val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error + f(a) + +def test2(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit = + x => y => swap(x, y) + foo(a)(a) // error + +def test3(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^) ->{x} Unit = + x => y => swap(x, y) + foo(a)(a) // error + +def test4(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^) ->{x} Unit = + x => y => swap(x, y) + val f = foo(a) + f(a) // error + +def test5(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^) ->{x} Unit = + x => y => swap(x, y) + val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // should be error, but we don't check params + f(a) From e943c1c56ec291bf0e437a58149c9b719e7b667d Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 22 Feb 2025 12:19:10 +0100 Subject: [PATCH 45/93] Use Fresh(...) instead of Fresh.Cap(...) Also, add a binder to Fresh.Annot for future use --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 16 ++--- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 26 +++---- .../dotty/tools/dotc/cc/CheckCaptures.scala | 16 ++--- .../src/dotty/tools/dotc/cc/Existential.scala | 12 ++-- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 71 ++++++++++--------- .../src/dotty/tools/dotc/cc/SepCheck.scala | 8 +-- .../tools/dotc/printing/PlainPrinter.scala | 6 +- 8 files changed, 81 insertions(+), 76 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 8f5320bcde7d..57d9e62feff9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -21,7 +21,7 @@ import CaptureSet.VarState /** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() -/** Context property to print Fresh.Cap as "fresh" instead of "cap" */ +/** Context property to print Fresh(...) as "fresh" instead of "cap" */ val PrintFresh: Key[Unit] = Key() object ccConfig: diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 5c0f1aa2e496..214e34e8808a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -92,12 +92,12 @@ trait CaptureRef extends TypeProxy, ValueType: case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false - /** Is this reference a Fresh.Cap instance? */ + /** Is this reference a Fresh instance? */ final def isFresh(using Context): Boolean = this match - case Fresh.Cap(_) => true + case Fresh(_) => true case _ => false - /** Is this reference the generic root capability `cap` or a Fresh.Cap instance? */ + /** Is this reference the generic root capability `cap` or a Fresh instance? */ final def isCapOrFresh(using Context): Boolean = isCap || isFresh /** Is this reference one of the generic root capabilities `cap` or `cap.rd` ? */ @@ -111,7 +111,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 Existential.Var(_) => true - case Fresh.Cap(_) => true + case Fresh(_) => true case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false @@ -244,9 +244,9 @@ trait CaptureRef extends TypeProxy, ValueType: /** This is a maximal capability that subsumes `y` in given context and VarState. * @param canAddHidden If true we allow maximal capabilities to subsume all other capabilities. - * We add those capabilities to the hidden set if this is Fresh.Cap + * We add those capabilities to the hidden set if this is a Fresh instance. * If false we only accept `y` elements that are already in the - * hidden set of this Fresh.Cap. The idea is that in a VarState that + * hidden set of this Fresh instance. The idea is that in a VarState that * accepts additions we first run `maxSubsumes` with `canAddHidden = false` * so that new variables get added to the sets. If that fails, we run * the test again with canAddHidden = true as a last effort before we @@ -255,7 +255,7 @@ trait CaptureRef extends TypeProxy, ValueType: def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = (this eq y) || this.match - case Fresh.Cap(hidden) => + case Fresh(hidden) => vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) || !y.stripReadOnly.isCap && canAddHidden && vs.addHidden(hidden, y) case _ => @@ -287,7 +287,7 @@ trait CaptureRef extends TypeProxy, ValueType: this match case MaybeCapability(x1) => x1.covers(y1) case _ => false - case Fresh.Cap(hidden) => + case Fresh(hidden) => hidden.superCaps.exists(this covers _) case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index d07edf63af83..675785a0cde2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -419,7 +419,7 @@ object CaptureSet: defn.captureRoot.termRef.singletonCaptureSet def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = - Fresh.Cap(owner).singletonCaptureSet + Fresh(owner).singletonCaptureSet /** The shared capture set `{cap.rd}` */ def shared(using Context): CaptureSet = @@ -650,7 +650,7 @@ object CaptureSet: def solve()(using Context): Unit = if !isConst then val approx = upperApprox(empty) - .map(Fresh.FromCap(NoSymbol).inverse) // Fresh.Cap --> cap + .map(Fresh.FromCap(NoSymbol).inverse) // Fresh --> cap .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems @@ -939,14 +939,14 @@ 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, + /** A capture set variable used to record the references hidden by a Fresh instance, * The elems and deps members are repurposed as follows: * elems: Set of hidden references - * deps : Set of hidden sets for which the Fresh.Cap instance owning this set + * deps : Set of hidden sets for which the Fresh instance owning this set * is a hidden element. * Hidden sets may become aliases of other hidden sets, which means that * reads and writes of elems go to the alias. - * If H is an alias of R.hidden for some Fresh.Cap R then: + * If H is an alias of R.hidden for some Fresh instance R then: * H.elems == {R} * H.deps = {R.hidden} * This encoding was chosen because it relies only on the elems and deps fields @@ -965,14 +965,14 @@ object CaptureSet: private def aliasRef: AnnotatedType | Null = if myElems.size == 1 then myElems.nth(0) match - case al @ Fresh.Cap(hidden) if deps.contains(hidden) => al + case al @ Fresh(hidden) if deps.contains(hidden) => al case _ => null else null private def aliasSet: HiddenSet = if myElems.size == 1 then myElems.nth(0) match - case Fresh.Cap(hidden) if deps.contains(hidden) => hidden + case Fresh(hidden) if deps.contains(hidden) => hidden case _ => this else this @@ -989,7 +989,7 @@ object CaptureSet: /** Add element to hidden set. Also add it to all supersets (as indicated by * deps of this set). Follow aliases on both hidden set and added element - * before adding. If the added element is also a Fresh.Cap instance with + * before adding. If the added element is also a Fresh instance with * hidden set H which is a superset of this set, then make this set an * alias of H. */ @@ -1003,7 +1003,7 @@ object CaptureSet: assert(dep != this) vs.addHidden(dep.asInstanceOf[HiddenSet], elem) elem match - case Fresh.Cap(hidden) => + case Fresh(hidden) => if this ne hidden then val alias = hidden.aliasRef if alias != null then @@ -1019,7 +1019,7 @@ object CaptureSet: addToElems() /** Apply function `f` to `elems` while setting `elems` to empty for the - * duration. This is used to escape infinite recursions if two Fresh.Caps + * duration. This is used to escape infinite recursions if two Freshs * refer to each other in their hidden sets. */ override def processElems[T](f: Refs => T): T = @@ -1192,7 +1192,7 @@ object CaptureSet: /** A class for states that do not allow to record elements or dependent sets. * In effect this means that no new elements or dependent sets can be added * in these states (since the previous state cannot be recorded in a snapshot) - * On the other hand, these states do allow by default Fresh.Cap instances to + * On the other hand, these states do allow by default Fresh instances to * subsume arbitary types, which are then recorded in their hidden sets. */ class Closed extends VarState: @@ -1201,7 +1201,7 @@ object CaptureSet: override def isOpen = false override def toString = "closed varState" - /** A closed state that allows a Fresh.Cap instance to subsume a + /** A closed state that allows a Fresh instance to subsume a * reference `r` only if `r` is already present in the hidden set of the instance. * No new references can be added. */ @@ -1210,7 +1210,7 @@ object CaptureSet: override def toString = "separating varState" override def isSeparating = true - /** A closed state that allows a Fresh.Cap instance to subsume a + /** A closed state that allows a Fresh instance to subsume a * reference `r` only if `r` is already present in the hidden set of the instance. * No new references can be added. */ diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 5f7277dc14df..bbdec9dd56c5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -326,7 +326,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) - /* Also set any previously unset owners of toplevel Fresh.Cap instances to improve + /* Also set any previously unset owners of toplevel Fresh instances to improve * error diagnostics in separation checking. */ private def anchorCaps(sym: Symbol)(using Context) = new TypeTraverser: @@ -336,7 +336,7 @@ class CheckCaptures extends Recheck, SymTransformer: case t @ CapturingType(parent, refs) => for ref <- refs.elems do ref match - case Fresh.Cap(hidden) if !hidden.givenOwner.exists => + case Fresh(hidden) if !hidden.givenOwner.exists => hidden.givenOwner = sym case _ => traverse(parent) @@ -346,7 +346,7 @@ class CheckCaptures extends Recheck, SymTransformer: traverseChildren(t) /** If `tpt` is an inferred type, interpolate capture set variables appearing contra- - * variantly in it. Also anchor Fresh.Cap instances with anchorCaps. + * variantly in it. Also anchor Fresh instances with anchorCaps. */ private def interpolateVarsIn(tpt: Tree, sym: Symbol)(using Context): Unit = if tpt.isInstanceOf[InferredTypeTree] then @@ -508,7 +508,7 @@ class CheckCaptures extends Recheck, SymTransformer: capt.println(i"Widen reach $c to $underlying in ${env.owner}") if ccConfig.useSepChecks then recur(underlying.filter(!_.isMaxCapability), env, null) - // we don't want to disallow underlying Fresh.Cap, since these are typically locally created + // we don't want to disallow underlying Fresh instances, since these are typically locally created // fresh capabilities. We don't need to also follow the hidden set since separation // checking makes ure that locally hidden references need to go to @consume parameters. else @@ -721,7 +721,7 @@ class CheckCaptures extends Recheck, SymTransformer: res /** Recheck argument against a "freshened" version of `formal` where toplevel `cap` - * occurrences are replaced by `Fresh.Cap`. Also, if formal parameter carries a `@use`, + * occurrences are replaced by `Fresh` instances. Also, if formal parameter carries a `@use`, * charge the deep capture set of the actual argument to the environment. */ protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = @@ -820,14 +820,14 @@ class CheckCaptures extends Recheck, SymTransformer: * * Second half: union of initial capture set and all capture sets of arguments * to tracked parameters. The initial capture set `initCs` is augmented with - * - Fresh.Cap if `core` extends Mutable - * - Fresh.Cap.rd if `core` extends Capability + * - Fresh(...) if `core` extends Mutable + * - Fresh(...).rd if `core` extends Capability */ def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = if core.derivesFromMutable then initCs ++ CaptureSet.fresh() - else if core.derivesFromCapability then initCs ++ Fresh.Cap(core.classSymbol).readOnly.singletonCaptureSet + else if core.derivesFromCapability then initCs ++ Fresh(core.classSymbol).readOnly.singletonCaptureSet else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 8ecb4570fcba..6bd2abb35865 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -259,19 +259,19 @@ object Existential: core /** Map existentially bound references referring to `boundVar` one-to-one - * to Fresh.Cap instances + * to Fresh instances */ def boundVarToCap(boundVar: TermParamRef, tp: Type)(using Context) = val subst = new IdempotentCaptRefMap: val seen = EqHashMap[Annotation, CaptureRef]() def apply(t: Type): Type = t match case t @ Var(`boundVar`) => - seen.getOrElseUpdate(t.annot, Fresh.Cap(NoSymbol)) + seen.getOrElseUpdate(t.annot, Fresh(NoSymbol)) case _ => mapOver(t) subst(tp) - /** Map top-level existentials to `Fresh.Cap`. */ + /** Map top-level existentials to `Fresh`. */ def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => boundVarToCap(boundVar, unpacked) @@ -281,7 +281,7 @@ object Existential: tp1.derivedAnnotatedType(toCap(parent), ann) case _ => tp - /** Map existentials at the top-level and in all nested result types to `Fresh.Cap` + /** Map existentials at the top-level and in all nested result types to `Fresh` */ def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => @@ -352,14 +352,14 @@ object Existential: def apply(t: Type) = t match case t @ Var(`boundVar`) => // do a reverse getOrElseUpdate on `seen` to produce the - // `Fresh.Cap` assosicated with `t` + // `Fresh` assosicated with `t` val it = seen.iterator var ref: CaptureRef | Null = null while it.hasNext && ref == null do val (k, v) = it.next if v.annot eq t.annot then ref = k if ref == null then - ref = Fresh.Cap(NoSymbol) + ref = Fresh(NoSymbol) seen(ref) = t ref case _ => mapOver(t) diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index d6693498e4b7..73e4950c1fd5 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -16,27 +16,56 @@ import util.SimpleIdentitySet.empty import CaptureSet.{Refs, emptyRefs, NarrowingCapabilityMap} import dotty.tools.dotc.util.SimpleIdentitySet -/** A module for handling Fresh types. Fresh.Cap instances are top type that keep +/** A module for handling Fresh types. Fresh instances are top types that keep * track of what they hide when capabilities get widened by subsumption to fresh. * The module implements operations to convert between regular caps.cap and - * Fresh.Cap instances. Fresh.Cap is encoded as `caps.cap @freshCapability(...)` where + * Fresh instances. Fresh(...) is encoded as `caps.cap @freshCapability(...)` where * `freshCapability(...)` is a special kind of annotation of type `Fresh.Annot` * that contains a hidden set. */ object Fresh: - /** The annotation of a Fresh.Cap instance */ - case class Annot(hidden: CaptureSet.HiddenSet) extends Annotation: + /** The annotation of a Fresh instance */ + case class Annot(hidden: CaptureSet.HiddenSet, binder: MethodType | NoType.type = NoType) 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 + def derivedAnnotation(binder: MethodType | NoType.type)(using Context): Annotation = + if this.binder eq binder then this else Annot(hidden, binder) + override def hash: Int = hidden.hashCode override def eql(that: Annotation) = that match - case Annot(hidden) => this.hidden eq hidden + case Annot(hidden, binder) => (this.hidden eq hidden) && (this.binder eq binder) case _ => false + + override def mapWith(tm: TypeMap)(using Context) = + tm(binder) match + case binder1: MethodType => derivedAnnotation(binder1) + case _ => this end Annot + /** Extractor methods for "fresh" capabilities */ + def apply(owner: Symbol, initialHidden: Refs = emptyRefs)(using Context): CaptureRef = + if ccConfig.useSepChecks then + val hiddenSet = CaptureSet.HiddenSet(owner, initialHidden) + val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet)) + hiddenSet.owningCap = res + //assert(hiddenSet.id != 3) + res + else + defn.captureRoot.termRef + + def apply(owner: Symbol, reach: Boolean)(using Context): CaptureRef = + apply(owner, ownerToHidden(owner, reach)) + + def apply(owner: Symbol)(using Context): CaptureRef = + apply(owner, ownerToHidden(owner, reach = false)) + + def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match + case Annot(hidden, _) => Some(hidden) + case _ => None + /** The initial elements (either 0 or 1) of a hidden set created for given `owner`. * If owner `x` is a trackable this is `x*` if reach` is true, or `x` otherwise. */ @@ -47,30 +76,6 @@ object Fresh: else if ref.isTracked then SimpleIdentitySet(ref) else emptyRefs - /** An extractor for "fresh" capabilities */ - object Cap: - - def apply(owner: Symbol, initialHidden: Refs = emptyRefs)(using Context): CaptureRef = - if ccConfig.useSepChecks then - val hiddenSet = CaptureSet.HiddenSet(owner, initialHidden) - val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet)) - hiddenSet.owningCap = res - //assert(hiddenSet.id != 3) - res - else - defn.captureRoot.termRef - - def apply(owner: Symbol, reach: Boolean)(using Context): CaptureRef = - apply(owner, ownerToHidden(owner, reach)) - - def apply(owner: Symbol)(using Context): CaptureRef = - apply(owner, ownerToHidden(owner, reach = false)) - - def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match - case Annot(hidden) => Some(hidden) - case _ => None - end Cap - /** Map each occurrence of cap to a different Sep.Cap instance */ class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: thisMap => @@ -81,7 +86,7 @@ object Fresh: if variance <= 0 then t else t match case t: CaptureRef if t.isCap => - Cap(owner, ownerToHidden(owner, reach)) + Fresh(owner, ownerToHidden(owner, reach)) case t @ CapturingType(_, refs) => val savedReach = reach if t.isBoxed then reach = true @@ -99,7 +104,7 @@ object Fresh: lazy val inverse: BiTypeMap & FollowAliasesMap = new BiTypeMap with FollowAliasesMap: def apply(t: Type): Type = t match - case t @ Cap(_) => defn.captureRoot.termRef + case t @ Fresh(_) => defn.captureRoot.termRef case t @ CapturingType(_, refs) => mapOver(t) case _ => mapFollowingAliases(t) @@ -118,14 +123,14 @@ object Fresh: /** 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 + * `Fresh` to be printed as `fresh` instead of `cap`, so that one avoids * confusion in error messages. */ def printContext(refs: (Type | CaptureSet)*)(using Context): Context = def hasCap = new TypeAccumulator[Boolean]: def apply(x: Boolean, t: Type) = x || t.dealiasKeepAnnots.match - case Fresh.Cap(_) => false + case Fresh(_) => false case t: TermRef => t.isCap || this(x, t.widen) case x: ThisType => false case _ => foldOver(x, t) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 036d12b94f4a..5221a46c5e2b 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -201,7 +201,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if seen.contains(newElem) then recur(seen, acc, newElems1) else newElem.stripReadOnly match - case Fresh.Cap(hidden) => + case Fresh(hidden) => if hidden.deps.isEmpty then recur(seen + newElem, acc + newElem, newElems1) else val superCaps = @@ -270,7 +270,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet def hiddenByElem(elem: CaptureRef): Refs = elem match - case Fresh.Cap(hcs) => hcs.elems ++ recur(hcs.elems) + case Fresh(hcs) => hcs.elems ++ recur(hcs.elems) case ReadOnlyCapability(ref1) => hiddenByElem(ref1).map(_.readOnly) case _ => emptyRefs @@ -318,7 +318,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def sharedPeaksStr(shared: Refs)(using Context): String = shared.nth(0) match - case fresh @ Fresh.Cap(hidden) => + case fresh @ Fresh(hidden) => if hidden.owner.exists then i"$fresh of ${hidden.owner}" else i"$fresh" case other => i"$other" @@ -591,7 +591,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: refs.deductSymFootprint(role.dclSym).deduct(explicitRefs(tpe)) /** Check validity of consumed references `refsToCheck`. The references are consumed - * because they are hidden in a Fresh.Cap result type or they are referred + * because they are hidden in a Fresh result type or they are referred * to in an argument to a @consume parameter or in a prefix of a @consume method -- * which one applies is determined by the role parameter. * diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index f054ed7fac3d..cdfc844e5cba 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -27,10 +27,10 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def printDebug = ctx.settings.YprintDebug.value - /** Print Fresh.Cap instances as */ + /** Print Fresh instances as */ protected def printFreshDetailed = ctx.settings.YccPrintFresh.value - /** Print Fresh.Cap instances as "fresh" */ + /** Print Fresh instances as "fresh" */ protected def printFresh = printFreshDetailed || ctx.property(PrintFresh).isDefined private var openRecs: List[RecType] = Nil @@ -442,7 +442,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" case Existential.Var(bv) => toTextRef(bv) - case Fresh.Cap(hidden) => + case Fresh(hidden) => val idStr = if showUniqueIds then s"#${hidden.id}" else "" if printFreshDetailed then s"" else if printFresh then "fresh" From 38d5177b445a6bc6cc178d3c556ff0265e652aff Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 23 Feb 2025 11:29:06 +0100 Subject: [PATCH 46/93] Print cc-generated dependent functions as parametric functions If a function got converted by Setup to a dependent function but is not really dependent, print is as a normal parametric function. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 8 +++ .../src/dotty/tools/dotc/core/Types.scala | 6 +++ .../tools/dotc/printing/RefinedPrinter.scala | 50 ++++++++++--------- tests/neg-custom-args/captures/byname.check | 4 +- tests/neg-custom-args/captures/levels.check | 2 +- tests/neg-custom-args/captures/reaches.check | 40 +++++++-------- tests/neg-custom-args/captures/reaches2.check | 28 +++++------ tests/neg-custom-args/captures/readOnly.check | 2 +- .../captures/sep-compose.check | 2 +- .../neg-custom-args/captures/use-capset.check | 4 +- .../captures/vars-simple.check | 2 +- tests/neg-custom-args/captures/vars.check | 4 +- 12 files changed, 84 insertions(+), 68 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 57d9e62feff9..a49b3832ff61 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -82,6 +82,14 @@ def depFun(args: List[Type], resultType: Type, isContextual: Boolean, paramNames else make(args, resultType) mt.toFunctionType(alwaysDependent = true) +/** This function has the form of a dependent function (i.e. it is a RefinedType with + * a MethodType refinement), but there are no dependencies and all parameter names + * are synthetic. + */ +def isNotReallyDependent(info: MethodType)(using Context): Boolean = + !info.looksDependent && info.paramNames.zipWithIndex.forall: (name, i) => + name == nme.syntheticParamName(i) + /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type)(using Context) extends Exception(tpe.show) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index ee6ee7bcd908..5a263c7fe227 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4053,6 +4053,12 @@ object Types extends TypeUtils { def isParamDependent(using Context): Boolean = paramDependencyStatus == TrueDeps || paramDependencyStatus == CaptureDeps + /** Like resultDependent || paramDependent, but without attempt to eliminate + * dependencies with de-aliasing + */ + def looksDependent(using Context): Boolean = + dependencyStatus != NoDeps || paramDependencyStatus != NoDeps + /** Is there a dependency involving a reference in a capture set, but * otherwise no true result dependency? */ diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index a6be756738eb..65a2a4895938 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -161,39 +161,41 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { private def toTextFunction(tp: AppliedType, refs: Text = Str("")): Text = val AppliedType(tycon, args) = (tp: @unchecked) val tsym = tycon.typeSymbol - val isGiven = tsym.name.isContextFunction + val isContextual = tsym.name.isContextFunction val capturesRoot = refs == rootSetText val isPure = Feature.pureFunsEnabled && !tsym.name.isImpureFunction && !capturesRoot - changePrec(GlobalPrec) { - val argStr: Text = - if args.length == 2 - && !defn.isDirectTupleNType(args.head) - && !isGiven - then - atPrec(InfixPrec) { argText(args.head) } - else + toTextFunction(args.init, args.last, refs.provided(!capturesRoot), isContextual, isPure) + + private def toTextFunction(args: List[Type], res: Type, refs: Text, + isContextual: Boolean, isPure: Boolean): Text = + changePrec(GlobalPrec): + val argStr: Text = args match + case arg :: Nil if !defn.isDirectTupleNType(arg) && !isContextual => + atPrec(InfixPrec): + argText(arg) + case _=> "(" - ~ argsText(args.init) + ~ argsText(args) ~ ")" - argStr - ~ " " ~ arrow(isGiven, isPure) - ~ (refs provided !capturesRoot) - ~ " " ~ argText(args.last) - } + argStr ~ " " ~ arrow(isContextual, isPure) ~ refs ~ " " ~ argText(res) protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = info match case info: MethodType => + val isContextual = info.isImplicitMethod val capturesRoot = refs == rootSetText - changePrec(GlobalPrec) { - "(" - ~ paramsText(info) - ~ ") " - ~ arrow(info.isImplicitMethod, isPure && !capturesRoot) - ~ (refs provided !capturesRoot) - ~ " " - ~ toTextMethodAsFunction(info.resultType, isPure) - } + if cc.isCaptureCheckingOrSetup && cc.isNotReallyDependent(info) then + // cc.Setup converts all functions to dependent functions. Undo that when printing. + toTextFunction(info.paramInfos, info.resType, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) + else + changePrec(GlobalPrec): + "(" + ~ paramsText(info) + ~ ") " + ~ arrow(isContextual, isPure && !capturesRoot) + ~ refs.provided(!capturesRoot) + ~ " " + ~ toTextMethodAsFunction(info.resultType, isPure) case info: PolyType => changePrec(GlobalPrec) { "[" diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index de2078ddf30a..0e1a016442ed 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -1,8 +1,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:10:6 ---------------------------------------- 10 | h(f2()) // error | ^^^^ - | Found: (x$0: Int) ->{cap1} Int - | Required: (x$0: Int) ->? Int + | Found: Int ->{cap1} Int + | Required: Int ->? Int | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index b99adefd4b2f..b9c388579386 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -7,7 +7,7 @@ 22 | r.setV(g) // error | ^ | Found: box (x: String) ->{cap3} String - | Required: box (x$0: String) ->? String + | Required: box String ->? String | | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set ? of value r diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 0f17402332f5..0524d4154aa3 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -64,31 +64,31 @@ -- Error: tests/neg-custom-args/captures/reaches.scala:80:28 ----------------------------------------------------------- 80 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ - | Separation failure: argument of type (x$0: A) ->{x} box A^? - | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C - | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? - | and hides capabilities {x}. - | Some of these overlap with the captures of the second argument with type (x$0: A) ->{y} box A^?. + | Separation failure: argument of type A ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type A ->{y} box A^?. | - | Hidden set of current argument : {x} - | Hidden footprint of current argument : {x, ps*} - | Capture set of second argument : {y} - | Footprint set of second argument : {y, ps*} - | The two sets overlap at : {ps*} + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} -- Error: tests/neg-custom-args/captures/reaches.scala:83:28 ----------------------------------------------------------- 83 | ps.map((x, y) => compose1(x, y)) // error sepcheck | ^ - | Separation failure: argument of type (x$0: A) ->{x} box A^? - | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C - | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? - | and hides capabilities {x}. - | Some of these overlap with the captures of the second argument with type (x$0: A) ->{y} box A^?. + | Separation failure: argument of type A ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type A ->{y} box A^?. | - | Hidden set of current argument : {x} - | Hidden footprint of current argument : {x, ps*} - | Capture set of second argument : {y} - | Footprint set of second argument : {y, ps*} - | The two sets overlap at : {ps*} + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} -- Error: tests/neg-custom-args/captures/reaches.scala:62:31 ----------------------------------------------------------- 62 | val leaked = usingFile[File^{id*}]: f => // error | ^^^ diff --git a/tests/neg-custom-args/captures/reaches2.check b/tests/neg-custom-args/captures/reaches2.check index c792d3c3a47d..926e6772bd8f 100644 --- a/tests/neg-custom-args/captures/reaches2.check +++ b/tests/neg-custom-args/captures/reaches2.check @@ -1,24 +1,24 @@ -- Error: tests/neg-custom-args/captures/reaches2.scala:10:10 ---------------------------------------------------------- 10 | ps.map((x, y) => compose1(x, y)) // error // error // error | ^ - |reference ps* is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? + | reference ps* is not included in the allowed capture set {} + | of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box A^? ->? A^? -- Error: tests/neg-custom-args/captures/reaches2.scala:10:13 ---------------------------------------------------------- 10 | ps.map((x, y) => compose1(x, y)) // error // error // error | ^ - |reference ps* is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? + | reference ps* is not included in the allowed capture set {} + | of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box A^? ->? A^? -- Error: tests/neg-custom-args/captures/reaches2.scala:10:28 ---------------------------------------------------------- 10 | ps.map((x, y) => compose1(x, y)) // error // error // error | ^ - | Separation failure: argument of type (x$0: A) ->{x} box A^? - | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C - | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? - | and hides capabilities {x}. - | Some of these overlap with the captures of the second argument with type (x$0: A) ->{y} box A^?. + | Separation failure: argument of type A ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type A ->{y} box A^?. | - | Hidden set of current argument : {x} - | Hidden footprint of current argument : {x, ps*} - | Capture set of second argument : {y} - | Footprint set of second argument : {y, ps*} - | The two sets overlap at : {ps*} + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} diff --git a/tests/neg-custom-args/captures/readOnly.check b/tests/neg-custom-args/captures/readOnly.check index e1aed07657e5..46721d3e1627 100644 --- a/tests/neg-custom-args/captures/readOnly.check +++ b/tests/neg-custom-args/captures/readOnly.check @@ -8,7 +8,7 @@ -- [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) + | Found: (putA : Int ->{a} Unit) | Required: Int -> Unit | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/sep-compose.check b/tests/neg-custom-args/captures/sep-compose.check index 305887f6b997..8a513690e18e 100644 --- a/tests/neg-custom-args/captures/sep-compose.check +++ b/tests/neg-custom-args/captures/sep-compose.check @@ -86,7 +86,7 @@ 40 | p1(f) // error | ^ | Separation failure: argument of type (f : () ->{a} Unit) - | to a function of type (x$0: () => Unit) ->{f} Unit + | to a function of type (() => Unit) ->{f} Unit | corresponds to capture-polymorphic formal parameter x$0 of type () => Unit | and hides capabilities {f}. | Some of these overlap with the captures of the function prefix. diff --git a/tests/neg-custom-args/captures/use-capset.check b/tests/neg-custom-args/captures/use-capset.check index 74afaa05890f..4897698e336d 100644 --- a/tests/neg-custom-args/captures/use-capset.check +++ b/tests/neg-custom-args/captures/use-capset.check @@ -13,7 +13,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/use-capset.scala:13:50 ----------------------------------- 13 | val _: () -> List[Object^{io}] -> Object^{io} = h2 // error, should be ->{io} | ^^ - | Found: (h2 : () ->? (x$0: List[box Object^{io}]^{}) ->{io} Object^{io}) - | Required: () -> List[box Object^{io}] -> Object^{io} + | Found: (h2 : () ->? List[box Object^{io}]^{} ->{io} Object^{io}) + | Required: () -> List[box Object^{io}] -> Object^{io} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/vars-simple.check b/tests/neg-custom-args/captures/vars-simple.check index 2bc014e9a4e7..71fab0dcf7d2 100644 --- a/tests/neg-custom-args/captures/vars-simple.check +++ b/tests/neg-custom-args/captures/vars-simple.check @@ -15,7 +15,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars-simple.scala:17:12 ---------------------------------- 17 | b = List(g) // error | ^^^^^^^ - | Found: List[box (x$0: String) ->{cap3} String] + | Found: List[box String ->{cap3} String] | Required: List[box String ->{cap1, cap2} String] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index db5c8083e3b7..a6c417db4838 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -9,7 +9,7 @@ 25 | a = g // error | ^ | Found: (x: String) ->{cap3} String - | Required: (x$0: String) ->{cap1} String + | Required: String ->{cap1} String | | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set {cap1} of variable a @@ -18,7 +18,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:27:12 ----------------------------------------- 27 | b = List(g) // error | ^^^^^^^ - | Found: List[box (x$0: String) ->{cap3} String] + | Found: List[box String ->{cap3} String] | Required: List[box String ->{cap1, cap2} String] | | longer explanation available when compiling with `-explain` From 2c073727c5154edd79da44081dfd25531fddda4d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 23 Feb 2025 17:01:16 +0100 Subject: [PATCH 47/93] Fix resultDependent computation for MethodTypes 1. We need to make the result Provisional if we encounter capture set variables 2. We don't do (1) for parameter dependencies since we assume that a method cannot become param-dependent by capture set inference 3. We drop the separate CaptureDependent state since it makes nowhere a difference. --- .../src/dotty/tools/dotc/core/Types.scala | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 5a263c7fe227..858d07545352 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3952,7 +3952,7 @@ object Types extends TypeUtils { def apply(tp: Type) = tp match { case tp @ TypeRef(pre, _) => tp.info match { - case TypeAlias(alias) if depStatus(NoDeps, pre) == TrueDeps => apply(alias) + case TypeAlias(alias) if depStatus(NoDeps, pre, forParams = false) == TrueDeps => apply(alias) case _ => mapOver(tp) } case _ => @@ -3966,7 +3966,7 @@ object Types extends TypeUtils { private var myDependencyStatus: DependencyStatus = Unknown private var myParamDependencyStatus: DependencyStatus = Unknown - private def depStatus(initial: DependencyStatus, tp: Type)(using Context): DependencyStatus = + private def depStatus(initial: DependencyStatus, tp: Type, forParams: Boolean)(using Context): DependencyStatus = class DepAcc extends TypeAccumulator[DependencyStatus]: def apply(status: DependencyStatus, tp: Type) = compute(status, tp, this) def combine(x: DependencyStatus, y: DependencyStatus) = @@ -3995,11 +3995,13 @@ object Types extends TypeUtils { case tp: AnnotatedType => tp match case CapturingType(parent, refs) => - (compute(status, parent, theAcc) /: refs.elems) { + val status1 = (compute(status, parent, theAcc) /: refs.elems): (s, ref) => ref.stripReach match - case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, CaptureDeps) + case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, TrueDeps) case tp => combine(s, compute(status, tp, theAcc)) - } + if refs.isConst || forParams // We assume capture set variables in parameters don't generate param dependencies + then status1 + else combine(status1, Provisional) case _ => if tp.annot.refersToParamOf(thisLambdaType) then TrueDeps else compute(status, tp.parent, theAcc) @@ -4023,7 +4025,7 @@ object Types extends TypeUtils { private def dependencyStatus(using Context): DependencyStatus = if (myDependencyStatus != Unknown) myDependencyStatus else { - val result = depStatus(NoDeps, resType) + val result = depStatus(NoDeps, resType, forParams = false) if ((result & Provisional) == 0) myDependencyStatus = result (result & StatusMask).toByte } @@ -4036,7 +4038,7 @@ object Types extends TypeUtils { else { val result = if (paramInfos.isEmpty) NoDeps - else paramInfos.tail.foldLeft(NoDeps)(depStatus(_, _)) + else paramInfos.tail.foldLeft(NoDeps)(depStatus(_, _, forParams = true)) if ((result & Provisional) == 0) myParamDependencyStatus = result (result & StatusMask).toByte } @@ -4045,24 +4047,20 @@ object Types extends TypeUtils { * which cannot be eliminated by de-aliasing? */ def isResultDependent(using Context): Boolean = - dependencyStatus == TrueDeps || dependencyStatus == CaptureDeps + dependencyStatus == TrueDeps /** Does one of the parameter types contain references to earlier parameters * of this method type which cannot be eliminated by de-aliasing? */ def isParamDependent(using Context): Boolean = - paramDependencyStatus == TrueDeps || paramDependencyStatus == CaptureDeps + paramDependencyStatus == TrueDeps /** Like resultDependent || paramDependent, but without attempt to eliminate * dependencies with de-aliasing */ def looksDependent(using Context): Boolean = - dependencyStatus != NoDeps || paramDependencyStatus != NoDeps - - /** Is there a dependency involving a reference in a capture set, but - * otherwise no true result dependency? - */ - def isCaptureDependent(using Context) = dependencyStatus == CaptureDeps + (dependencyStatus & StatusMask) != NoDeps + || (paramDependencyStatus & StatusMask) != NoDeps def newParamRef(n: Int): TermParamRef = new TermParamRefImpl(this, n) @@ -4470,8 +4468,7 @@ object Types extends TypeUtils { final val Unknown: DependencyStatus = 0 // not yet computed final val NoDeps: DependencyStatus = 1 // no dependent parameters found final val FalseDeps: DependencyStatus = 2 // all dependent parameters are prefixes of non-depended alias types - final val CaptureDeps: DependencyStatus = 3 // dependencies in capture sets under captureChecking, otherwise only false dependencoes - final val TrueDeps: DependencyStatus = 4 // some truly dependent parameters exist + final val TrueDeps: DependencyStatus = 3 // some truly dependent parameters exist final val StatusMask: DependencyStatus = 7 // the bits indicating actual dependency status final val Provisional: DependencyStatus = 8 // set if dependency status can still change due to type variable instantiations } From 263f1ba0ea73fd8e482898cd0e7e1352276495a8 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 23 Feb 2025 20:27:37 +0100 Subject: [PATCH 48/93] Adopt declared parameter names in comparison When comparing actual vs expected functions where expected parameter names are synthetic but actual parameter names are not, use the actual parameter names in the expected type. This makes type mismatch error messages line up better. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 8 -------- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 8 ++++++++ compiler/src/dotty/tools/dotc/core/Types.scala | 5 ++++- .../src/dotty/tools/dotc/printing/RefinedPrinter.scala | 2 +- tests/neg-custom-args/captures/levels.check | 2 +- tests/neg-custom-args/captures/vars.check | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a49b3832ff61..57d9e62feff9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -82,14 +82,6 @@ def depFun(args: List[Type], resultType: Type, isContextual: Boolean, paramNames else make(args, resultType) mt.toFunctionType(alwaysDependent = true) -/** This function has the form of a dependent function (i.e. it is a RefinedType with - * a MethodType refinement), but there are no dependencies and all parameter names - * are synthetic. - */ -def isNotReallyDependent(info: MethodType)(using Context): Boolean = - !info.looksDependent && info.paramNames.zipWithIndex.forall: (name, i) => - name == nme.syntheticParamName(i) - /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type)(using Context) extends Exception(tpe.show) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index bbdec9dd56c5..c2ee1185c917 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1305,6 +1305,14 @@ class CheckCaptures extends Recheck, SymTransformer: case defn.RefinedFunctionOf(rinfo: MethodType) => depFun(args, resultType, isContextual, rinfo.paramNames) case _ => expected + case expected @ defn.RefinedFunctionOf(einfo: MethodType) + if einfo.allParamNamesSynthetic => + actual match + case defn.RefinedFunctionOf(ainfo: MethodType) + if !ainfo.allParamNamesSynthetic && ainfo.paramNames.hasSameLengthAs(einfo.paramNames) => + einfo.derivedLambdaType(paramNames = ainfo.paramNames) + .toFunctionType(alwaysDependent = true) + case _ => expected case _ => expected recur(expected) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 858d07545352..866d9ba21cb4 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4090,6 +4090,10 @@ object Types extends TypeUtils { } dropDependencies(resultType) else resultType + + /** Are all parameter names synthetic? */ + def allParamNamesSynthetic = paramNames.zipWithIndex.forall: (name, i) => + name == nme.syntheticParamName(i) } abstract case class MethodType(paramNames: List[TermName])( @@ -4119,7 +4123,6 @@ object Types extends TypeUtils { def nonErasedParamCount(using Context): Int = paramInfos.count(p => !p.hasAnnotation(defn.ErasedParamAnnot)) - protected def prefixString: String = companion.prefixString } diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 65a2a4895938..c0c5c43d21f7 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -184,7 +184,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case info: MethodType => val isContextual = info.isImplicitMethod val capturesRoot = refs == rootSetText - if cc.isCaptureCheckingOrSetup && cc.isNotReallyDependent(info) then + if cc.isCaptureCheckingOrSetup && info.allParamNamesSynthetic && !info.looksDependent then // cc.Setup converts all functions to dependent functions. Undo that when printing. toTextFunction(info.paramInfos, info.resType, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) else diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index b9c388579386..96512055e119 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -7,7 +7,7 @@ 22 | r.setV(g) // error | ^ | Found: box (x: String) ->{cap3} String - | Required: box String ->? String + | Required: box (x: String) ->? String | | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set ? of value r diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index a6c417db4838..a05eefd16622 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -9,7 +9,7 @@ 25 | a = g // error | ^ | Found: (x: String) ->{cap3} String - | Required: String ->{cap1} String + | Required: (x: String) ->{cap1} String | | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set {cap1} of variable a From 4f348a7392a47c64bfc470ae0afd4c07f64566c2 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 24 Feb 2025 15:59:43 +0100 Subject: [PATCH 49/93] Make mapping to dependent functions more uniform 1. Do the mapping for inferred as well as explicit types 2. Avoid the mapping if the result type does not carry any capture sets The idea is that we want to have the same representation for all functions that have capture sets somewhere in their result. That way, we can use the MethodType representing the function as a binder for revised existentials instead of creating a separate binder for them. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 76 +++++++++---------- .../src/dotty/tools/dotc/cc/Synthetics.scala | 9 +-- .../captures/cc-ex-conformance.scala | 31 -------- .../captures/sep-compose.check | 2 +- 4 files changed, 39 insertions(+), 79 deletions(-) delete mode 100644 tests/neg-custom-args/captures/cc-ex-conformance.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 84d1f5068b1a..9d5d98550cac 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -180,6 +180,32 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp: MethodOrPoly => tp // don't box results of methods outside refinements case _ => recur(tp) + trait SetupTypeMap extends FollowAliasesMap: + private var isTopLevel = true + + protected def innerApply(tp: Type): Type + + final def apply(tp: Type) = + val saved = isTopLevel + if variance < 0 then isTopLevel = false + try innerApply(tp) + finally isTopLevel = saved + + protected def normalizeFunctions(tp: Type, original: Type)(using Context): Type = tp match + case AppliedType(tycon, args) + if defn.isNonRefinedFunction(tp) && isTopLevel => + original match + case AppliedType(`tycon`, args0) if args0.last ne args.last => + // We have an applied type that underwent some addition of capture sets. + // Map to a dependent type so that things are more uniform. + depFun(args.init, args.last, + isContextual = defn.isContextFunctionClass(tycon.classSymbol)) + .showing(i"add function refinement $tp ($tycon, ${args.init}, ${args.last}) --> $result", capt) + case _ => tp + case _ => tp + + end SetupTypeMap + /** Transform the type of an InferredTypeTree by performing the following transformation * steps everywhere in the type: * 1. Drop retains annotations @@ -197,7 +223,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ private def transformInferredType(tp: Type)(using Context): Type = - def mapInferred(refine: Boolean): TypeMap = new TypeMap with FollowAliasesMap: + def mapInferred(refine: Boolean): TypeMap = new TypeMap with SetupTypeMap: override def toString = "map inferred" /** Refine a possibly applied class type C where the class has tracked parameters @@ -225,42 +251,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => tp case _ => tp - private var isTopLevel = true - - private def mapNested(ts: List[Type]): List[Type] = - val saved = isTopLevel - isTopLevel = false - try ts.mapConserve(this) - finally isTopLevel = saved - - def apply(tp: Type) = + def innerApply(tp: Type) = val tp1 = tp match case AnnotatedType(parent, annot) if annot.symbol.isRetains => // Drop explicit retains annotations apply(parent) - case tp @ AppliedType(tycon, args) => - val tycon1 = this(tycon) - if defn.isNonRefinedFunction(tp) then - // Convert toplevel generic function types to dependent functions - if !defn.isFunctionSymbol(tp.typeSymbol) && (tp.dealias ne tp) then - // This type is a function after dealiasing, so we dealias and recurse. - // See #15925. - this(tp.dealias) - else - val args0 = args.init - var res0 = args.last - val args1 = mapNested(args0) - val res1 = this(res0) - if isTopLevel then - depFun(args1, res1, - isContextual = defn.isContextFunctionClass(tycon1.classSymbol)) - .showing(i"add function refinement $tp ($tycon1, $args1, $res1) (${tp.dealias}) --> $result", capt) - else if (tycon1 eq tycon) && (args1 eq args0) && (res1 eq res0) then - tp - else - tp.derivedAppliedType(tycon1, args1 :+ res1) - else - tp.derivedAppliedType(tycon1, args.mapConserve(arg => box(this(arg)))) case defn.RefinedFunctionOf(rinfo: MethodType) => val rinfo1 = apply(rinfo) if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) @@ -268,10 +263,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case Existential(_, unpacked) => // drop the existential, the bound variables will be replaced by capture set variables apply(unpacked) - case tp: MethodType => - tp.derivedLambdaType( - paramInfos = mapNested(tp.paramInfos), - resType = this(tp.resType)) case tp: TypeLambda => // Don't recurse into parameter bounds, just cleanup any stray retains annotations tp.derivedLambdaType( @@ -279,8 +270,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: resType = this(tp.resType)) case _ => mapFollowingAliases(tp) - addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner) - end apply + addVar( + addCaptureRefinements(normalizeCaptures(normalizeFunctions(tp1, tp))), + ctx.owner) end mapInferred try @@ -307,7 +299,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def fail(msg: Message) = if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) - val toCapturing = new DeepTypeMap with FollowAliasesMap: + val toCapturing = new DeepTypeMap with SetupTypeMap: override def toString = "expand aliases" /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib @@ -361,7 +353,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => tp - def apply(t: Type) = + def innerApply(t: Type) = t match case t @ CapturingType(parent, refs) => checkSharedOK: @@ -393,7 +385,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: && (!sym.isConstructor || (t ne tp.finalResultType)) // Don't add ^ to result types of class constructors deriving from Capability then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) - else normalizeCaptures(mapFollowingAliases(t)) + else normalizeCaptures(normalizeFunctions(mapFollowingAliases(t), t)) end toCapturing val tp1 = toCapturing(tp) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index cfdcbbc401bf..82dea9616017 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -140,11 +140,10 @@ object Synthetics: def transformCurriedTupledCaptures(info: Type, owner: Symbol) = val (et: ExprType) = info: @unchecked val (enclThis: ThisType) = owner.thisType: @unchecked - def mapFinalResult(tp: Type, f: Type => Type): Type = - val defn.FunctionNOf(args, res, isContextual) = tp: @unchecked - if defn.isFunctionNType(res) then - defn.FunctionNOf(args, mapFinalResult(res, f), isContextual) - else + def mapFinalResult(tp: Type, f: Type => Type): Type = tp match + case FunctionOrMethod(args, res) => + tp.derivedFunctionOrMethod(args, mapFinalResult(res, f)) + case _ => f(tp) ExprType(mapFinalResult(et.resType, CapturingType(_, CaptureSet(enclThis)))) diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.scala b/tests/neg-custom-args/captures/cc-ex-conformance.scala deleted file mode 100644 index 8f3efc46fb06..000000000000 --- a/tests/neg-custom-args/captures/cc-ex-conformance.scala +++ /dev/null @@ -1,31 +0,0 @@ -// This contains a lot of illegal capture ref errors, which should be treated as -// noise. The problem is that we can't write an existential type by hand, -// sincxe existentially typed variables carry an @existential annotation, which -// can't be written down. The interesting errors are the rest. - -import language.experimental.captureChecking -import caps.{Exists, Capability, existential} - - -class C - -type EX1 = () => (c: Exists) => (C^{c}, C^{c}) // error: illegal capture ref - -type EX2 = () => (c1: Exists) => (c2: Exists) => (C^{c1}, C^{c2}) // error: illegal capture ref - -type EX3 = () => (c: Exists) => (x: Object^) => C^{c} // error: illegal capture ref - -type EX4 = () => (x: Object^) => (c: Exists) => C^{c} // error: illegal capture ref - -def Test = - val ex1: EX1 = ??? // error: illegal capture ref - val ex2: EX2 = ??? // error: illegal capture ref - val _: EX1 = ex1 // error: illegal capture ref - val _: EX2 = ex1 // error separation // error: illegal capture ref - val _: EX1 = ex2 // error: illegal capture ref - - val ex3: EX3 = ??? // error: illegal capture ref - val ex4: EX4 = ??? // error: illegal capture ref - val _: EX4 = ex3 // error: illegal capture ref - val _: EX4 = ex4 // error: illegal capture ref - val _: EX3 = ex4 // error: type mismatch // error: illegal capture ref diff --git a/tests/neg-custom-args/captures/sep-compose.check b/tests/neg-custom-args/captures/sep-compose.check index 8a513690e18e..459f00789ea8 100644 --- a/tests/neg-custom-args/captures/sep-compose.check +++ b/tests/neg-custom-args/captures/sep-compose.check @@ -87,7 +87,7 @@ | ^ | Separation failure: argument of type (f : () ->{a} Unit) | to a function of type (() => Unit) ->{f} Unit - | corresponds to capture-polymorphic formal parameter x$0 of type () => Unit + | corresponds to capture-polymorphic formal parameter v1 of type () => Unit | and hides capabilities {f}. | Some of these overlap with the captures of the function prefix. | From 5b6533f8e837cecf1bae427c40084bf204da8bbf Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 24 Feb 2025 16:13:30 +0100 Subject: [PATCH 50/93] Refactoring: Move normalizeCaptures to SetupTypeMap --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 71 ++++++++++---------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 9d5d98550cac..05425e4af9b6 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -180,7 +180,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp: MethodOrPoly => tp // don't box results of methods outside refinements case _ => recur(tp) - trait SetupTypeMap extends FollowAliasesMap: + private trait SetupTypeMap extends FollowAliasesMap: private var isTopLevel = true protected def innerApply(tp: Type): Type @@ -191,6 +191,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: try innerApply(tp) finally isTopLevel = saved + /** Map parametric functions with results that have a capture set somewhere + * to dependent functions. + */ protected def normalizeFunctions(tp: Type, original: Type)(using Context): Type = tp match case AppliedType(tycon, args) if defn.isNonRefinedFunction(tp) && isTopLevel => @@ -204,6 +207,38 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => tp case _ => tp + /** Pull out an embedded capture set from a part of `tp` */ + def normalizeCaptures(tp: Type)(using Context): Type = tp match + case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => + CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) + case tp: RecType => + tp.parent match + case parent @ CapturingType(parent1, refs) => + CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed) + case _ => + tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created + // by `mapInferred`. Hence if the underlying type admits capture variables + // a variable was already added, and the first case above would apply. + case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => + CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) + case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => + CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) + case tp @ AppliedType(tycon, args) + if !defn.isFunctionClass(tp.dealias.typeSymbol) && (tp.dealias eq tp) => + tp.derivedAppliedType(tycon, args.mapConserve(box)) + case tp: RealTypeBounds => + tp.derivedTypeBounds(tp.lo, box(tp.hi)) + case tp: LazyRef => + normalizeCaptures(tp.ref) + case _ => + tp + end SetupTypeMap /** Transform the type of an InferredTypeTree by performing the following transformation @@ -292,7 +327,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 3. Add universal capture sets to types deriving from Capability * 4. Map `cap` in function result types to existentially bound variables. * 5. Schedule deferred well-formed tests for types with retains annotations. - * 6. Perform normalizeCaptures + * 6. Perform normalizeCaptures */ private def transformExplicitType(tp: Type, sym: Symbol, tptToCheck: Tree = EmptyTree)(using Context): Type = @@ -857,38 +892,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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) => - CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) - case tp: RecType => - tp.parent match - case parent @ CapturingType(parent1, refs) => - CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed) - case _ => - tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created - // by `mapInferred`. Hence if the underlying type admits capture variables - // a variable was already added, and the first case above would apply. - case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => - assert(tp1.isBoxed == tp2.isBoxed) - CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) - case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => - assert(tp1.isBoxed == tp2.isBoxed) - CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) - case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => - CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) - case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => - CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) - case tp @ AppliedType(tycon, args) - if !defn.isFunctionClass(tp.dealias.typeSymbol) && (tp.dealias eq tp) => - tp.derivedAppliedType(tycon, args.mapConserve(box)) - case tp: RealTypeBounds => - tp.derivedTypeBounds(tp.lo, box(tp.hi)) - case tp: LazyRef => - normalizeCaptures(tp.ref) - case _ => - tp - /** Run setup on a compilation unit with given `tree`. * @param recheckDef the function to run for completing a val or def */ From 8f0759431f69ce0b2869afa1fb8e91c00553559a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 24 Feb 2025 17:40:34 +0100 Subject: [PATCH 51/93] Fix: Avoid double refinements In transformExplicitType when transforming a dependent function type, we first converted the parametric parent type to be dependent and then added the refinement again, lading to two apply refinements. We now special-case the RefinedFunctionOf case in both SetupTypeMaps. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 05425e4af9b6..a8e68debf10c 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -188,7 +188,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: final def apply(tp: Type) = val saved = isTopLevel if variance < 0 then isTopLevel = false - try innerApply(tp) + try tp match + case defn.RefinedFunctionOf(rinfo: MethodType) => + val rinfo1 = apply(rinfo) + if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) + else tp + case _ => + innerApply(tp) finally isTopLevel = saved /** Map parametric functions with results that have a capture set somewhere @@ -291,10 +297,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case AnnotatedType(parent, annot) if annot.symbol.isRetains => // Drop explicit retains annotations apply(parent) - case defn.RefinedFunctionOf(rinfo: MethodType) => - val rinfo1 = apply(rinfo) - if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) - else tp case Existential(_, unpacked) => // drop the existential, the bound variables will be replaced by capture set variables apply(unpacked) From 64d99c5b2b8101c9e151ce3b85a5b0bdd729d236 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 24 Feb 2025 17:46:11 +0100 Subject: [PATCH 52/93] Simplify Existential.mapCapInResults --- .../src/dotty/tools/dotc/cc/Existential.scala | 23 ++++++--------- tests/neg-custom-args/captures/capt1.check | 16 +++++------ .../captures/depfun-reach.check | 4 +-- .../captures/existential-mapping.check | 28 +++++++++---------- tests/neg-custom-args/captures/i19330.check | 4 +-- tests/neg-custom-args/captures/i21401.check | 4 +-- tests/neg-custom-args/captures/i21614.check | 2 +- 7 files changed, 38 insertions(+), 43 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 6bd2abb35865..e9f046b6cad2 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -372,20 +372,15 @@ object Existential: end mapCap /** Map `cap` in function results to fresh existentials */ - def mapCapInResults(fail: Message => Unit)(using Context): TypeMap = new: - - def mapFunOrMethod(tp: Type, args: List[Type], res: Type): Type = - val args1 = atVariance(-variance)(args.map(this)) - val res1 = res match - case res: MethodType => mapFunOrMethod(res, res.paramInfos, res.resType) - case res: PolyType => mapFunOrMethod(res, Nil, res.resType) // TODO: Also map bounds of PolyTypes - case _ => mapCap(apply(res), fail) - //.showing(i"map cap res $res / ${apply(res)} of $tp = $result") - tp.derivedFunctionOrMethod(args1, res1) - + def mapCapInResults(fail: Message => Unit)(using Context): TypeMap = new TypeMap with FollowAliasesMap: def apply(t: Type): Type = t match - case FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => - mapFunOrMethod(t, args, res) + case defn.RefinedFunctionOf(mt) => + val mt1 = apply(mt) + if mt1 ne mt then mt1.toFunctionType(alwaysDependent = true) + else t + case t: MethodType if variance > 0 && !t.resType.isInstanceOf[MethodOrPoly] => + val t1 = mapOver(t).asInstanceOf[MethodType] + t1.derivedLambdaType(resType = mapCap(t1.resType, fail)) case CapturingType(parent, refs) => t.derivedCapturingType(this(parent), refs) case Existential(_, _) => @@ -393,7 +388,7 @@ object Existential: case t: (LazyRef | TypeVar) => mapConserveSuper(t) case _ => - mapOver(t) + mapFollowingAliases(t) end mapCapInResults /** Is `mt` a method represnting an existential type when used in a refinement? */ diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index d9b10129e3f9..979bb17e2031 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -36,33 +36,33 @@ -- Error: tests/neg-custom-args/captures/capt1.scala:36:16 ------------------------------------------------------------- 36 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^ - | Type variable X of method h cannot be instantiated to () -> (ex$18: caps.Exists) -> C^{ex$18} since - | the part C^{ex$18} of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to () -> (ex$14: caps.Exists) -> C^{ex$14} since + | the part C^{ex$14} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:36:30 ------------------------------------------------------------- 36 | val z2 = h[() -> Cap](() => x) // error // error | ^ | reference (x : C^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> (ex$18: caps.Exists) -> C^{ex$18} + | of an enclosing function literal with expected type () -> (ex$14: caps.Exists) -> C^{ex$14} -- Error: tests/neg-custom-args/captures/capt1.scala:38:13 ------------------------------------------------------------- 38 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ - | Type variable X of method h cannot be instantiated to box () ->{x} (ex$23: caps.Exists) -> C^{ex$23} since - | the part C^{ex$23} of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to box () ->{x} (ex$19: caps.Exists) -> C^{ex$19} since + | the part C^{ex$19} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:43:7 -------------------------------------------------------------- 43 | if x == null then // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | of value z1 with type () => (ex$23: caps.Exists) -> C^{ex$23}. | This type hides capabilities {x} -- Error: tests/neg-custom-args/captures/capt1.scala:44:12 ------------------------------------------------------------- 44 | () => x // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | of value z1 with type () => (ex$23: caps.Exists) -> C^{ex$23}. | This type hides capabilities {x} -- Error: tests/neg-custom-args/captures/capt1.scala:47:2 -------------------------------------------------------------- 47 | x // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | of value z1 with type () => (ex$23: caps.Exists) -> C^{ex$23}. | This type hides capabilities {x} diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index fc0e6c237647..bbe9347e5d6a 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -2,14 +2,14 @@ 13 | op // error | ^^ | Found: (xs: List[(X, box () ->{io} Unit)]) ->{op} List[box () ->{xs*} Unit] - | Required: (xs: List[(X, box () ->{io} Unit)]) ->{fresh} List[() -> Unit] + | Required: (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:20:60 --------------------------------- 20 | val b: (xs: List[() ->{io} Unit]) => List[() ->{} Unit] = a // error | ^ | Found: (xs: List[box () ->{io} Unit]) ->{a} List[box () ->{xs*} Unit] - | Required: (xs: List[box () ->{io} Unit]) ->{fresh} List[() -> Unit] + | Required: (xs: List[box () ->{io} Unit]) => List[() -> Unit] | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/depfun-reach.scala:12:17 ------------------------------------------------------ diff --git a/tests/neg-custom-args/captures/existential-mapping.check b/tests/neg-custom-args/captures/existential-mapping.check index b52fdb5750ed..6a0da29d7ce5 100644 --- a/tests/neg-custom-args/captures/existential-mapping.check +++ b/tests/neg-custom-args/captures/existential-mapping.check @@ -19,70 +19,70 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:15:30 -------------------------- 15 | val _: A^ -> (x: C^) -> C = x3 // error | ^^ - | Found: (x3 : A^ -> (x: C^) -> (ex$11: caps.Exists) -> C^{ex$11}) + | Found: (x3 : A^ -> (x: C^) -> (ex$9: caps.Exists) -> C^{ex$9}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:18:25 -------------------------- 18 | val _: A^ -> C^ -> C = x4 // error | ^^ - | Found: (x4 : A^ -> C^ -> (ex$19: caps.Exists) -> C^{ex$19}) + | Found: (x4 : A^ -> C^ -> (ex$17: caps.Exists) -> C^{ex$17}) | Required: A^ -> C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:21:30 -------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (x: C^) -> (ex$27: caps.Exists) -> C^{ex$27}) + | Found: (x5 : A^ -> (x: C^) -> (ex$23: caps.Exists) -> C^{ex$23}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:24:30 -------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} (ex$35: caps.Exists) -> C^{ex$35}) - | Required: A^ -> (ex$39: caps.Exists) -> (x: C^) ->{ex$39} C + | Found: (x6 : A^ -> (ex$32: caps.Exists) -> (x: C^) ->{ex$32} (ex$31: caps.Exists) -> C^{ex$31}) + | Required: A^ -> (ex$35: caps.Exists) -> (x: C^) ->{ex$35} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:27:25 -------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) ->{fresh} (ex$41: caps.Exists) -> C^{ex$41}) + | Found: (y1 : (x: C^) ->{fresh} (ex$37: caps.Exists) -> C^{ex$37}) | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:30:20 -------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ ->{fresh} (ex$45: caps.Exists) -> C^{ex$45}) + | Found: (y2 : C^ ->{fresh} (ex$41: caps.Exists) -> C^{ex$41}) | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:33:30 -------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ ->{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 + | Found: (y3 : A^ ->{fresh} (ex$44: caps.Exists) -> (x: C^) ->{ex$44} (ex$43: caps.Exists) -> C^{ex$43}) + | Required: A^ ->{fresh} (ex$47: caps.Exists) -> (x: C^) ->{ex$47} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:36:25 -------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ ->{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 + | Found: (y4 : A^ ->{fresh} (ex$50: caps.Exists) -> C^ ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) + | Required: A^ ->{fresh} (ex$52: caps.Exists) -> C^ ->{ex$52} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:39:30 -------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ ->{fresh} (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) + | Found: (y5 : A^ ->{fresh} (x: C^) -> (ex$54: caps.Exists) -> C^{ex$54}) | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:42:30 -------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ ->{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 + | Found: (y6 : A^ ->{fresh} (ex$63: caps.Exists) -> (x: C^) ->{ex$63} (ex$62: caps.Exists) -> C^{ex$62}) + | Required: A^ ->{fresh} (ex$66: caps.Exists) -> (x: C^) ->{ex$66} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index 894dd53bd303..7e26d6e929a1 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -7,11 +7,11 @@ 22 | val bad: bar.T = foo(bar) // error | ^^^^^^^^ | Found: () => Logger^ - | Required: () ->{fresh} (ex$9: caps.Exists) -> Logger^{ex$9} + | Required: () ->{fresh} (ex$7: caps.Exists) -> Logger^{ex$7} | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/i19330.scala:16:14 ------------------------------------------------------------ 16 | val t: () => Logger^ = () => l // error | ^^^^^^^^^^^^^ - | Separation failure: value t's type () => (ex$5: caps.Exists) -> Logger^{ex$5} hides parameter l. + | Separation failure: value t's type () => (ex$3: caps.Exists) -> Logger^{ex$3} hides parameter l. | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index 71680300046c..94ab4b94b57e 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -21,5 +21,5 @@ -- Error: tests/neg-custom-args/captures/i21401.scala:17:52 ------------------------------------------------------------ 17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error | ^^^^^^^^^^^^^^^^^^^^^^^^ - |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> (ex$20: caps.Exists) -> Boxed[box IO^{ex$20}] since - |the part box IO^{ex$20} of that type captures the root capability `cap`. + |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> (ex$14: caps.Exists) -> Boxed[box IO^{ex$14}] since + |the part box IO^{ex$14} of that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index aa60c2eaa366..c25417ed5454 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -8,7 +8,7 @@ -- [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.rd, _$1} + |Found: (_$1: box File^{files*}) ->{files*} (ex$14: caps.Exists) -> box Logger{val f: File^{_$1}}^{ex$14.rd, _$1} |Required: (_$1: box File^{files*}) => box Logger{val f: File^?}^? | |Note that the universal capability `cap.rd` From 3bd4bda2221471654c638d32f0ac36b61fc4c2a1 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 24 Feb 2025 17:50:12 +0100 Subject: [PATCH 53/93] Some infrastructure for making existentials Fresh instances --- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 15 +++++++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 29 ++++++++++++++------ tests/neg-custom-args/captures/i19330.check | 2 +- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 73e4950c1fd5..bc333b4d6fe1 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -26,7 +26,7 @@ import dotty.tools.dotc.util.SimpleIdentitySet object Fresh: /** The annotation of a Fresh instance */ - case class Annot(hidden: CaptureSet.HiddenSet, binder: MethodType | NoType.type = NoType) extends Annotation: + case class Annot(hidden: CaptureSet.HiddenSet, binder: MethodType | NoType.type) 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 @@ -45,11 +45,11 @@ object Fresh: case _ => this end Annot - /** Extractor methods for "fresh" capabilities */ + /** Constructor and extractor methods for "fresh" capabilities */ def apply(owner: Symbol, initialHidden: Refs = emptyRefs)(using Context): CaptureRef = if ccConfig.useSepChecks then val hiddenSet = CaptureSet.HiddenSet(owner, initialHidden) - val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet)) + val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet, NoType)) hiddenSet.owningCap = res //assert(hiddenSet.id != 3) res @@ -63,9 +63,16 @@ object Fresh: apply(owner, ownerToHidden(owner, reach = false)) def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match - case Annot(hidden, _) => Some(hidden) + case Annot(hidden, binder) if !binder.exists => Some(hidden) case _ => None + /** Create an existential */ + def existential(binder: MethodType)(using Context): AnnotatedType = + val hiddenSet = CaptureSet.HiddenSet(NoSymbol, emptyRefs) + val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet, binder)) + hiddenSet.owningCap = res + res + /** The initial elements (either 0 or 1) of a hidden set created for given `owner`. * If owner `x` is a trackable this is `x*` if reach` is true, or `x` otherwise. */ diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index a8e68debf10c..a368b6df920b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -390,6 +390,18 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => tp + /** Map references to capability classes C to C^, + * normalize captures and map to dependent functions. + */ + def defaultApply(t: Type) = + if t.derivesFromCapability + && !t.isSingleton + && t.typeSymbol != defn.Caps_Exists + && (!sym.isConstructor || (t ne tp.finalResultType)) + // Don't add ^ to result types of class constructors deriving from Capability + then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) + else normalizeCaptures(normalizeFunctions(mapFollowingAliases(t), t)) + def innerApply(t: Type) = t match case t @ CapturingType(parent, refs) => @@ -414,15 +426,16 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: t.derivedAnnotatedType(parent1, ann) case throwsAlias(res, exc) => this(expandThrowsAlias(res, exc, Nil)) + case t @ AppliedType(tycon, args) + if defn.isNonRefinedFunction(tp) + && !defn.isFunctionSymbol(tp.typeSymbol) && (tp.dealias ne tp) => + // Expand arguments of aliases of function types before proceeding with dealias. + // This is necessary to bind existentialFresh instances to the right method binder. + val args1 = atVariance(-variance): + args.map(this) + defaultApply(t.derivedAppliedType(tycon, args1)) case t => - // Map references to capability classes C to C^ - if t.derivesFromCapability - && !t.isSingleton - && t.typeSymbol != defn.Caps_Exists - && (!sym.isConstructor || (t ne tp.finalResultType)) - // Don't add ^ to result types of class constructors deriving from Capability - then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) - else normalizeCaptures(normalizeFunctions(mapFollowingAliases(t), t)) + defaultApply(t) end toCapturing val tp1 = toCapturing(tp) diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index 7e26d6e929a1..7f59697cdd87 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -7,7 +7,7 @@ 22 | val bad: bar.T = foo(bar) // error | ^^^^^^^^ | Found: () => Logger^ - | Required: () ->{fresh} (ex$7: caps.Exists) -> Logger^{ex$7} + | Required: () ->{fresh} Logger^{fresh} | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/i19330.scala:16:14 ------------------------------------------------------------ From ea5a9d5780328ca670645d729f19100e6f174a3d Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Feb 2025 13:35:04 +0100 Subject: [PATCH 54/93] Switch to new scheme for bound caps Drop old-style existentials and use instead Fresh instances that refer to that MethodType which defines the scope of the Fresh as its result type. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 14 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 11 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 37 ++-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 8 +- .../src/dotty/tools/dotc/cc/Existential.scala | 192 ++++++++++++++---- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 9 +- .../src/dotty/tools/dotc/cc/SepCheck.scala | 3 +- .../dotty/tools/dotc/core/Definitions.scala | 2 +- .../dotty/tools/dotc/core/Substituters.scala | 4 +- .../dotty/tools/dotc/core/TypeComparer.scala | 63 +++--- .../tools/dotc/printing/PlainPrinter.scala | 15 +- .../tools/dotc/printing/RefinedPrinter.scala | 57 +++--- .../immutable/LazyListIterable.scala | 20 +- tests/neg-custom-args/captures/capt1.check | 18 +- .../captures/existential-mapping.check | 32 +-- .../captures/heal-tparam-cs.scala | 15 +- tests/neg-custom-args/captures/i19330.check | 6 +- tests/neg-custom-args/captures/i21401.check | 4 +- tests/neg-custom-args/captures/i21614.check | 8 +- tests/neg-custom-args/captures/i21920.check | 2 +- .../captures/leaked-curried.check | 8 +- tests/neg-custom-args/captures/sep-use2.check | 51 +++++ tests/neg-custom-args/captures/try.check | 15 +- .../captures/unsound-reach-4.check | 2 +- tests/neg-custom-args/captures/vars.check | 13 +- 25 files changed, 427 insertions(+), 182 deletions(-) create mode 100644 tests/neg-custom-args/captures/sep-use2.check diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 57d9e62feff9..f00dea192ab7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -195,7 +195,7 @@ extension (tp: Type) */ final def isTrackableRef(using Context): Boolean = tp match case _: ThisType => true - case tp: TermParamRef => !Existential.isBinder(tp) + case tp: TermParamRef => !Existential.isBinderOLD(tp) case tp: TermRef => ((tp.prefix eq NoPrefix) || tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef @@ -205,7 +205,8 @@ extension (tp: Type) tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet) case tp: TypeParamRef => tp.derivesFrom(defn.Caps_CapSet) - case Existential.Var(_) => true + case Existential.VarOLD(_) => true + case Existential.Vble(_) => true case AnnotatedType(parent, annot) => defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef case _ => @@ -526,11 +527,11 @@ extension (tp: Type) case t @ AnnotatedType(parent, ann) => // Don't map annotations, which includes capture sets t.derivedAnnotatedType(this(parent), ann) - case t @ FunctionOrMethod(args, res @ Existential(_, _)) + case t @ FunctionOrMethod(args, res) if args.forall(_.isAlwaysPure) => // Also map existentials in results to reach capabilities if all // preceding arguments are known to be always pure - apply(t.derivedFunctionOrMethod(args, Existential.toCap(res))) + t.derivedFunctionOrMethod(args, apply(Existential.toCap(res))) case Existential(_, _) => t case _ => @@ -572,6 +573,11 @@ extension (tp: Type) case tp: ThisType => tp.cls.ccLevel.nextInner case _ => undefinedLevel + def hasSuffix(other: MethodType)(using Context): Boolean = + (tp eq other) || tp.match + case tp: MethodOrPoly => tp.resType.hasSuffix(other) + case _ => false + extension (cls: ClassSymbol) def pureBaseClass(using Context): Option[Symbol] = diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 214e34e8808a..00e38ece7832 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -6,7 +6,7 @@ import core.* import Types.*, Symbols.*, Contexts.*, Decorators.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.subsumesExistentially +import TypeComparer.subsumesExistentiallyOLD import util.common.alwaysTrue import scala.collection.mutable import CCState.* @@ -110,7 +110,8 @@ 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 Existential.Var(_) => true + case Existential.VarOLD(_) => true + case Existential.Vble(_) => true case Fresh(_) => true case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false @@ -228,7 +229,7 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false || this.match case ReachCapability(x1) => x1.subsumes(y.stripReach) - case Existential.Var(bv) => subsumesExistentially(bv, y) + case Existential.VarOLD(bv) => subsumesExistentiallyOLD(bv, y) case x: TermRef => viaInfo(x.info)(subsumingRefs(_, y)) case x: TypeRef if assumedContainsOf(x).contains(y) => true case x: TypeRef if x.derivesFrom(defn.Caps_CapSet) => @@ -258,6 +259,10 @@ trait CaptureRef extends TypeProxy, ValueType: case Fresh(hidden) => vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) || !y.stripReadOnly.isCap && canAddHidden && vs.addHidden(hidden, y) + case Existential.Vble(binder) => + y.stripReadOnly match + case Existential.Vble(binder1) => binder1.hasSuffix(binder) + case _ => true case _ => this.isCap && canAddHidden && vs != VarState.HardSeparate || y.match diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 675785a0cde2..64c29be1b582 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -104,7 +104,8 @@ sealed abstract class CaptureSet extends Showable: final def isUnboxable(using Context) = elems.exists: - case Existential.Var(_) => true + case Existential.VarOLD(_) => true + case Existential.Vble(_) => true case elem => elem.isRootCapability final def isReadOnly(using Context): Boolean = @@ -547,7 +548,7 @@ object CaptureSet: final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen addIfHiddenOrFail(elem) - else if Existential.isBadExistential(elem) then // Fail if `elem` is an out-of-scope existential + else if Existential.isBadExistentialOLD(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. @@ -567,15 +568,21 @@ object CaptureSet: elems -= elem res.addToTrace(this) + // TODO: Also track allowable TermParamRefs and Existential.Vbles in capture sets private def levelOK(elem: CaptureRef)(using Context): Boolean = if elem.isRootCapability then !noUniversal else elem match - case Existential.Var(bv) => + case Existential.VarOLD(bv) => !noUniversal && !TypeComparer.isOpenedExistential(bv) // Opened existentials on the left cannot be added to nested capture sets on the right // of a comparison. Test case is open-existential.scala. + case elem @ Existential.Vble(mt) => + !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. case elem: TermRef if level.isDefined => elem.prefix match case prefix: CaptureRef => @@ -629,7 +636,8 @@ object CaptureSet: try val approx = computeApprox(origin).ensuring(_.isConst) if approx.elems.exists: - case Existential.Var(_) => true + case Existential.VarOLD(_) => true + case Existential.Vble(_) => true case _ => false then ccState.approxWarnings += @@ -1304,7 +1312,7 @@ object CaptureSet: ref1.captureSetOfInfo.map(ReadOnlyMap()) case _ => if ref.isMaxCapability then ref.singletonCaptureSet - else ofType(ref.underlying, followResult = true) + else ofType(ref.underlying, followResult = true) // TODO: why followResult = true? /** Capture set of a type */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = @@ -1317,7 +1325,9 @@ object CaptureSet: case tp: (TypeRef | TypeParamRef) => if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet else empty - case tp @ Existential.Var(_) => + case tp @ Existential.VarOLD(_) => + tp.captureSet + case tp @ Existential.Vble(_) => tp.captureSet case CapturingType(parent, refs) => recur(parent) ++ refs @@ -1332,8 +1342,11 @@ object CaptureSet: CaptureSet.ofTypeDeeply(parent.widen) case tpd @ defn.RefinedFunctionOf(rinfo: MethodType) if followResult => ofType(tpd.parent, followResult = false) // pick up capture set from parent type - ++ (recur(rinfo.resType) // add capture set of result - -- CaptureSet(rinfo.paramRefs.filter(_.isTracked)*)) // but disregard bound parameters + ++ recur(rinfo.resType) // add capture set of result + .filter: + case TermParamRef(binder, _) => binder ne rinfo + case Existential.Vble(binder) => binder ne rinfo + case _ => true case tpd @ AppliedType(tycon, args) => if followResult && defn.isNonRefinedFunction(tpd) then recur(args.last) @@ -1359,7 +1372,7 @@ object CaptureSet: * capture sets. Nested existential sets are approximated with `cap`. * NOTE: The traversal logic needs to be in sync with narrowCaps in CaptureOps, which * replaces caps with reach capabilties. The one exception to this is invariant - * arguments. This have to be included to be conservative in dcs but must be + * arguments. These have to be included to be conservative in dcs but must be * excluded in narrowCaps. */ def ofTypeDeeply(tp: Type, includeTypevars: Boolean = false)(using Context): CaptureSet = @@ -1377,9 +1390,9 @@ object CaptureSet: val upper = t.info.bounds.hi if includeTypevars && upper.isExactlyAny then CaptureSet.fresh(t.symbol) else this(cs, upper) - case t @ FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - this(cs, Existential.toCap(res)) + case t @ FunctionOrMethod(args, res) => + if args.forall(_.isAlwaysPure) then this(cs, Existential.toCap(res)) + else cs case t @ Existential(_, _) => cs case _ => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c2ee1185c917..d94152d4920a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -23,7 +23,7 @@ import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} -import Existential.derivedExistentialType +import Existential.derivedExistentialTypeOLD import Annotations.Annotation /** The capture checker */ @@ -850,7 +850,7 @@ class CheckCaptures extends Recheck, SymTransformer: // added capture set to result. augmentConstructorType(parent, initCs ++ refs) case core @ Existential(boundVar, core1) => - core.derivedExistentialType(augmentConstructorType(core1, initCs)) + core.derivedExistentialTypeOLD(augmentConstructorType(core1, initCs)) case _ => val (refined, cs) = addParamArgRefinements(core, initCs) refined.capturing(cs) @@ -922,7 +922,7 @@ class CheckCaptures extends Recheck, SymTransformer: // which are less intelligible. An example is the line `a = x` in // neg-custom-args/captures/vars.scala. That's why this code is conditioned. // to apply only to closures that are not eta expansions. - val res1 = Existential.toCapDeeply(res) + val res1 = Existential.toCapDeeply(res) // TODO: why toCapDeeply? val pt1 = Existential.toCapDeeply(pt) // We need to open existentials here in order not to get vars mixed up in them // We do the proper check with existentials when we are finished with the closure block. @@ -1433,7 +1433,7 @@ class CheckCaptures extends Recheck, SymTransformer: // Get existentials and wildcards out of the way actual match case actual @ Existential(_, actualUnpacked) => - return Existential.derivedExistentialType(actual): + return Existential.derivedExistentialTypeOLD(actual): recur(actualUnpacked, expected, covariant) case _ => expected match diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index e9f046b6cad2..2fa9233819df 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -13,7 +13,7 @@ import NameKinds.ExistentialBinderName import NameOps.isImpureFunction import CaptureSet.IdempotentCaptRefMap import reporting.Message -import util.EqHashMap +import util.{SimpleIdentitySet, EqHashMap} import util.Spans.NoSpan /** @@ -210,17 +210,17 @@ Expansion of ^: */ object Existential: - type Carrier = RefinedType + type CarrierOLD = RefinedType - def unapply(tp: Carrier)(using Context): Option[(TermParamRef, Type)] = + def unapply(tp: CarrierOLD)(using Context): Option[(TermParamRef, Type)] = tp.refinedInfo match case mt: MethodType - if isExistentialMethod(mt) && defn.isNonRefinedFunction(tp.parent) => + if isExistentialMethodOLD(mt) && defn.isNonRefinedFunction(tp.parent) => Some(mt.paramRefs.head, mt.resultType) case _ => None /** Create method type in the refinement of an existential type */ - private def exMethodType(using Context)( + private def exMethodTypeOLD(using Context)( mk: TermParamRef => Type, boundName: TermName = ExistentialBinderName.fresh() ): MethodType = @@ -229,73 +229,119 @@ object Existential: mt => mk(mt.paramRefs.head)) /** Create existential */ - def apply(mk: TermParamRef => Type)(using Context): Type = - exMethodType(mk).toFunctionType(alwaysDependent = true) + def applyOLD(mk: TermParamRef => Type)(using Context): Type = + exMethodTypeOLD(mk).toFunctionType(alwaysDependent = true) /** The (super-) type of existentially bound references */ - type Var = AnnotatedType + type VarOLD = AnnotatedType /** An extractor for existentially bound references of the form ex @existential * where ex is a TermParamRef of type Exists */ - object Var: - def apply(boundVar: TermParamRef)(using Context): Var = + object VarOLD: + def apply(boundVar: TermParamRef)(using Context): VarOLD = AnnotatedType(boundVar, Annotation(defn.ExistentialAnnot, NoSpan)) - def unapply(tp: Var)(using Context): Option[TermParamRef] = tp match + def unapply(tp: VarOLD)(using Context): Option[TermParamRef] = tp match case AnnotatedType(bv: TermParamRef, ann) if ann.symbol == defn.ExistentialAnnot => Some(bv) case _ => None + /** The (super-) type of existentially bound references */ + type Vble = AnnotatedType + + object Vble: + def apply(mt: MethodType)(using Context): Vble = + Fresh.existential(mt) + def unapply(tp: Vble)(using Context): Option[MethodType] = tp.annot match + case ann: Fresh.Annot => + ann.binder match + case mt: MethodType => + assert(ann.hidden.elems.isEmpty) + Some(mt) + case _ => None + case _ => None + /** Create existential if bound variable appears in result of `mk` */ - def wrap(mk: TermParamRef => Type)(using Context): Type = - val mt = exMethodType(mk) + def wrapOLD(mk: TermParamRef => Type)(using Context): Type = + val mt = exMethodTypeOLD(mk) if mt.isResultDependent then mt.toFunctionType() else mt.resType - extension (tp: Carrier) - def derivedExistentialType(core: Type)(using Context): Type = tp match + extension (tp: CarrierOLD) + def derivedExistentialTypeOLD(core: Type)(using Context): Type = tp match case Existential(boundVar, unpacked) => if core eq unpacked then tp - else apply(bv => core.substParam(boundVar, bv)) + else applyOLD(bv => core.substParam(boundVar, bv)) case _ => core /** Map existentially bound references referring to `boundVar` one-to-one - * to Fresh instances + * to Fresh instances (OLD) */ - def boundVarToCap(boundVar: TermParamRef, tp: Type)(using Context) = + def boundVarToCapOLD(boundVar: TermParamRef, tp: Type)(using Context) = val subst = new IdempotentCaptRefMap: val seen = EqHashMap[Annotation, CaptureRef]() def apply(t: Type): Type = t match - case t @ Var(`boundVar`) => + case t @ VarOLD(`boundVar`) => seen.getOrElseUpdate(t.annot, Fresh(NoSymbol)) case _ => mapOver(t) subst(tp) - /** Map top-level existentials to `Fresh`. */ - def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match - case Existential(boundVar, unpacked) => - boundVarToCap(boundVar, unpacked) + /** Map existentially bound references referring to `boundVar` one-to-one + * to Fresh instances + */ + def toCapOLD(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match + case Existential(boundVar, unpacked) => // (OLD) + boundVarToCapOLD(boundVar, unpacked) case tp1 @ CapturingType(parent, refs) => tp1.derivedCapturingType(toCap(parent), refs) case tp1 @ AnnotatedType(parent, ann) => tp1.derivedAnnotatedType(toCap(parent), ann) case _ => tp + /** Map top-level free existential variables one-to-one to Fresh instances */ + def toCap(tp: Type, deep: Boolean = false)(using Context): Type = + val subst = new IdempotentCaptRefMap: + val seen = EqHashMap[Annotation, CaptureRef]() + var localBinders: SimpleIdentitySet[MethodType] = SimpleIdentitySet.empty + + def apply(t: Type): Type = t match + case t @ Vble(binder) => + if localBinders.contains(binder) then t // keep bound references + else seen.getOrElseUpdate(t.annot, Fresh(NoSymbol)) // map free references to Fresh() + case t: MethodType => + // skip parameters + val saved = localBinders + if !t.resType.isInstanceOf[MethodOrPoly] && !deep then localBinders = localBinders + t + try t.derivedLambdaType(resType = this(t.resType)) + finally localBinders = saved + case t: PolyType => + // skip parameters + t.derivedLambdaType(resType = this(t.resType)) + case _ => + mapOver(t) + + if ccConfig.newScheme then subst(tp) + else toCapOLD(tp) + /** Map existentials at the top-level and in all nested result types to `Fresh` */ - def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match + def toCapDeeplyOLD(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - toCapDeeply(boundVarToCap(boundVar, unpacked)) + toCapDeeplyOLD(boundVarToCapOLD(boundVar, unpacked)) case tp1 @ FunctionOrMethod(args, res) => - val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) + val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeplyOLD(res)) if tp2 ne tp1 then tp2 else tp case tp1 @ CapturingType(parent, refs) => - tp1.derivedCapturingType(toCapDeeply(parent), refs) + tp1.derivedCapturingType(toCapDeeplyOLD(parent), refs) case tp1 @ AnnotatedType(parent, ann) => - tp1.derivedAnnotatedType(toCapDeeply(parent), ann) + tp1.derivedAnnotatedType(toCapDeeplyOLD(parent), ann) case _ => tp - /** Knowing that `tp` is a function type, is an alias to a function other + def toCapDeeply(tp: Type)(using Context): Type = + if ccConfig.newScheme then toCap(tp, deep = true) + else toCapDeeplyOLD(tp) + + /** Knowing that `tp` is a function type, is it an alias to a function other * than `=>`? */ private def isAliasFun(tp: Type)(using Context) = tp match @@ -307,7 +353,7 @@ object Existential: * capture set variables, create an existential with the variable wrapping the type. * Stop at function or method types since these have been mapped before. */ - def mapCap(tp: Type, fail: Message => Unit)(using Context): Type = + def mapCapOLD(tp: Type, fail: Message => Unit)(using Context): Type = var needsWrap = false abstract class CapMap extends BiTypeMap: @@ -322,13 +368,13 @@ object Existential: super.mapOver(t) class Wrap(boundVar: TermParamRef) extends CapMap: - private val seen = EqHashMap[CaptureRef, Var]() + private val seen = EqHashMap[CaptureRef, VarOLD]() def apply(t: Type) = t match case t: CaptureRef if t.isCapOrFresh => if variance > 0 then needsWrap = true - seen.getOrElseUpdate(t, Var(boundVar)) + seen.getOrElseUpdate(t, VarOLD(boundVar)) else if variance == 0 then fail(em"""$tp captures the root capability `cap` in invariant position""") @@ -342,7 +388,7 @@ object Existential: needsWrap = true super.mapOver: defn.FunctionNOf(args, res, contextual) - .capturing(Var(boundVar).singletonCaptureSet) + .capturing(VarOLD(boundVar).singletonCaptureSet) else mapOver(t) case _ => mapOver(t) @@ -350,7 +396,7 @@ object Existential: lazy val inverse = new BiTypeMap: def apply(t: Type) = t match - case t @ Var(`boundVar`) => + case t @ VarOLD(`boundVar`) => // do a reverse getOrElseUpdate on `seen` to produce the // `Fresh` assosicated with `t` val it = seen.iterator @@ -367,8 +413,68 @@ object Existential: override def toString = "Wrap.inverse" end Wrap - val wrapped = apply(Wrap(_)(tp)) + val wrapped = applyOLD(Wrap(_)(tp)) if needsWrap then wrapped else tp + end mapCapOLD + + /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound + * variable bound by `mt`. + * Stop at function or method types since these have been mapped before. + */ + def mapCap(tp: Type, mt: MethodType, fail: Message => Unit)(using Context): Type = + + abstract class CapMap extends BiTypeMap: + override def mapOver(t: Type): Type = t match + case t @ FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => + t // `t` should be mapped in this case by a different call to `mapCap`. + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) + case _ => + super.mapOver(t) + + object toVar extends CapMap: + private val seen = EqHashMap[CaptureRef, Vble]() + + def apply(t: Type) = t match + case t: CaptureRef if t.isCapOrFresh => + if variance > 0 then + seen.getOrElseUpdate(t, Vble(mt)) + else + if variance == 0 then + fail(em"""$tp captures the root capability `cap` in invariant position""") + // we accept variance < 0, and leave the cap as it is + super.mapOver(t) + case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => + if variance > 0 then + super.mapOver: + defn.FunctionNOf(args, res, contextual) + .capturing(Vble(mt).singletonCaptureSet) + else mapOver(t) + case _ => + mapOver(t) + //.showing(i"mapcap $t = $result") + override def toString = "toVar" + + object inverse extends BiTypeMap: + def apply(t: Type) = t match + case t @ Vble(`mt`) => + // do a reverse getOrElseUpdate on `seen` to produce the + // `Fresh` assosicated with `t` + val it = seen.iterator + var ref: CaptureRef | Null = null + while it.hasNext && ref == null do + val (k, v) = it.next + if v.annot eq t.annot then ref = k + if ref == null then + ref = Fresh(NoSymbol) + seen(ref) = t + ref + case _ => mapOver(t) + def inverse = toVar.this + override def toString = "toVar.inverse" + end toVar + + toVar(tp) end mapCap /** Map `cap` in function results to fresh existentials */ @@ -380,7 +486,9 @@ object Existential: else t case t: MethodType if variance > 0 && !t.resType.isInstanceOf[MethodOrPoly] => val t1 = mapOver(t).asInstanceOf[MethodType] - t1.derivedLambdaType(resType = mapCap(t1.resType, fail)) + t1.derivedLambdaType(resType = + if ccConfig.newScheme then mapCap(t1.resType, t1, fail) + else mapCapOLD(t1.resType, fail)) case CapturingType(parent, refs) => t.derivedCapturingType(this(parent), refs) case Existential(_, _) => @@ -392,22 +500,22 @@ object Existential: end mapCapInResults /** Is `mt` a method represnting an existential type when used in a refinement? */ - def isExistentialMethod(mt: TermLambda)(using Context): Boolean = mt.paramInfos match + def isExistentialMethodOLD(mt: TermLambda)(using Context): Boolean = mt.paramInfos match case (info: TypeRef) :: rest => info.symbol == defn.Caps_Exists && rest.isEmpty case _ => false /** Is `ref` a TermParamRef representing existentially bound variables? */ - def isBinder(ref: CaptureRef)(using Context) = ref match - case ref: TermParamRef => isExistentialMethod(ref.binder) + def isBinderOLD(ref: CaptureRef)(using Context) = ref match + case ref: TermParamRef => isExistentialMethodOLD(ref.binder) case _ => false /** An value signalling an out-of-scope existential that should * lead to a compare failure. */ - def badExistential(using Context): TermParamRef = - exMethodType(identity, nme.OOS_EXISTENTIAL).paramRefs.head + def badExistentialOLD(using Context): TermParamRef = + exMethodTypeOLD(identity, nme.OOS_EXISTENTIAL).paramRefs.head - def isBadExistential(ref: CaptureRef) = ref match + def isBadExistentialOLD(ref: CaptureRef) = ref match case ref: TermParamRef => ref.paramName == nme.OOS_EXISTENTIAL case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index bc333b4d6fe1..6d3209302e78 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -39,10 +39,11 @@ object Fresh: case Annot(hidden, binder) => (this.hidden eq hidden) && (this.binder eq binder) case _ => false - override def mapWith(tm: TypeMap)(using Context) = - tm(binder) match - case binder1: MethodType => derivedAnnotation(binder1) - case _ => this + override def mapWith(tm: TypeMap)(using Context) = tm match + case tm: Substituters.SubstBindingMap[MethodType] @unchecked if tm.from eq binder => + derivedAnnotation(tm.to) + case _ => + this end Annot /** Constructor and extractor methods for "fresh" capabilities */ diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 5221a46c5e2b..f355db76954b 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -196,7 +196,8 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: recur(elems, elems.toList) private def peaks(using Context): Refs = - def recur(seen: Refs, acc: Refs, newElems: List[CaptureRef]): Refs = newElems match + def recur(seen: Refs, acc: Refs, newElems: List[CaptureRef]): Refs = trace(i"peaks $acc, $newElems = "): + newElems match case newElem :: newElems1 => if seen.contains(newElem) then recur(seen, acc, newElems1) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 37959f1dc768..b4a243cc40b8 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1228,7 +1228,7 @@ class Definitions { case mt: MethodType if tpe.refinedName == nme.apply && isFunctionType(tpe.parent) - && !Existential.isExistentialMethod(mt) => Some(mt) + && !Existential.isExistentialMethodOLD(mt) => Some(mt) case mt: PolyType if tpe.refinedName == nme.apply && isFunctionType(tpe.parent) => Some(mt) diff --git a/compiler/src/dotty/tools/dotc/core/Substituters.scala b/compiler/src/dotty/tools/dotc/core/Substituters.scala index 96da91293d91..be474215fbb8 100644 --- a/compiler/src/dotty/tools/dotc/core/Substituters.scala +++ b/compiler/src/dotty/tools/dotc/core/Substituters.scala @@ -9,7 +9,7 @@ import cc.CaptureSet.IdempotentCaptRefMap */ object Substituters: - final def subst(tp: Type, from: BindingType, to: BindingType, theMap: SubstBindingMap | Null)(using Context): Type = + final def subst[BT <: BindingType](tp: Type, from: BT, to: BT, theMap: SubstBindingMap[BT] | Null)(using Context): Type = tp match { case tp: BoundType => if (tp.binder eq from) tp.copyBoundType(to.asInstanceOf[tp.BT]) else tp @@ -163,7 +163,7 @@ object Substituters: .mapOver(tp) } - final class SubstBindingMap(from: BindingType, to: BindingType)(using Context) extends DeepTypeMap, BiTypeMap { + final class SubstBindingMap[BT <: BindingType](val from: BT, val to: BT)(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) def inverse = SubstBindingMap(to, from) } diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 99a187a51d70..04e9383d20ef 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -47,8 +47,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling monitored = false GADTused = false opaquesUsed = false - openedExistentials = Nil - assocExistentials = Nil + openedExistentialsOLD = Nil + assocExistentialsOLD = Nil recCount = 0 needsGc = false if Config.checkTypeComparerReset then checkReset() @@ -70,14 +70,15 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** In capture checking: The existential types that are open because they * appear in an existential type on the left in an enclosing comparison. */ - private var openedExistentials: List[TermParamRef] = Nil + private var openedExistentialsOLD: List[TermParamRef] = Nil + private var openedMethods: List[MethodType] = Nil /** In capture checking: A map from existential types that are appear * in an existential type on the right in an enclosing comparison. * Each existential gets mapped to the opened existentials to which it * may resolve at this point. */ - private var assocExistentials: ExAssoc = Nil + private var assocExistentialsOLD: ExAssocOLD = Nil private var myInstance: TypeComparer = this def currentInstance: TypeComparer = myInstance @@ -822,7 +823,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling (tp1.signature consistentParams tp2.signature) && matchingMethodParams(tp1, tp2) && (!tp2.isImplicitMethod || tp1.isImplicitMethod) && - isSubType(tp1.resultType, tp2.resultType.subst(tp2, tp1)) + inOpenedMethod(tp2): + isSubType(tp1.resultType, tp2.resultType.subst(tp2, tp1)) case _ => false } compareMethod @@ -2809,23 +2811,28 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** A type associating instantiatable existentials on the right of a comparison * with the existentials they can be instantiated with. */ - type ExAssoc = List[(TermParamRef, List[TermParamRef])] + type ExAssocOLD = List[(TermParamRef, List[TermParamRef])] private def compareExistentialLeft(boundVar: TermParamRef, tp1unpacked: Type, tp2: Type)(using Context): Boolean = - val saved = openedExistentials + val saved = openedExistentialsOLD try - openedExistentials = boundVar :: openedExistentials + openedExistentialsOLD = boundVar :: openedExistentialsOLD recur(tp1unpacked, tp2) finally - openedExistentials = saved + openedExistentialsOLD = saved private def compareExistentialRight(tp1: Type, boundVar: TermParamRef, tp2unpacked: Type)(using Context): Boolean = - val saved = assocExistentials + val saved = assocExistentialsOLD try - assocExistentials = (boundVar, openedExistentials) :: assocExistentials + assocExistentialsOLD = (boundVar, openedExistentialsOLD) :: assocExistentialsOLD recur(tp1, tp2unpacked) finally - assocExistentials = saved + assocExistentialsOLD = saved + + private def inOpenedMethod[T](mt: MethodType)(op: => T)(using Context): T = + val saved = openedMethods + if !mt.resType.isInstanceOf[MethodOrPoly] then openedMethods = mt :: openedMethods + try op finally openedMethods = saved /** Is `tp1` an existential var that subsumes `tp2`? This is the case if `tp1` is * instantiatable (i.e. it's a key in `assocExistentials`) and one of the @@ -2837,11 +2844,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * EX c1: A -> Ex c2. B */ def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean = - def canInstantiateWith(assoc: ExAssoc): Boolean = assoc match + def canInstantiateWith(assoc: ExAssocOLD): Boolean = assoc match case (bv, bvs) :: assoc1 => if bv == tp1 then tp2 match - case Existential.Var(bv2) => + case Existential.VarOLD(bv2) => bvs.contains(bv2) || assoc1.exists(_._1 == bv2) case _ => true @@ -2850,34 +2857,36 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case Nil => false tp2 match - case Existential.Var(bv2) if tp1 eq bv2 => + case Existential.VarOLD(bv2) if tp1 eq bv2 => true // for now, existential references referring to the same // binder are identified. !!! TODO this needs to be revised case _ => - canInstantiateWith(assocExistentials) + canInstantiateWith(assocExistentialsOLD) def isOpenedExistential(ref: CaptureRef)(using Context): Boolean = - openedExistentials.contains(ref) + ref match + case Existential.Vble(mt) => openedMethods.contains(mt) + case _ => openedExistentialsOLD.contains(ref) /** bi-map taking existentials to the left of a comparison to matching * existentials on the right. This is not a bijection. However * we have `forwards(backwards(bv)) == bv` for an existentially bound `bv`. * That's enough to qualify as a BiTypeMap. */ - private class MapExistentials(assoc: ExAssoc)(using Context) extends BiTypeMap: + private class MapExistentials(assoc: ExAssocOLD)(using Context) extends BiTypeMap: private def bad(t: Type) = - Existential.badExistential + Existential.badExistentialOLD .showing(i"existential match not found for $t in $assoc", capt) def apply(t: Type) = t match - case t: TermParamRef if Existential.isBinder(t) => + case t: TermParamRef if Existential.isBinderOLD(t) => // Find outermost existential on the right that can be instantiated to `t`, // or `badExistential` if none exists. - def findMapped(assoc: ExAssoc): CaptureRef = assoc match + def findMapped(assoc: ExAssocOLD): CaptureRef = assoc match case (bv, assocBvs) :: assoc1 => val outer = findMapped(assoc1) - if !Existential.isBadExistential(outer) then outer + if !Existential.isBadExistentialOLD(outer) then outer else if assocBvs.contains(t) then bv else bad(t) case Nil => @@ -2891,7 +2900,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling */ lazy val inverse = new BiTypeMap: def apply(t: Type) = t match - case t: TermParamRef if Existential.isBinder(t) => + case t: TermParamRef if Existential.isBinderOLD(t) => assoc.find(_._1 == t) match case Some((_, bvs)) if bvs.nonEmpty => bvs.head case _ => bad(t) @@ -2909,11 +2918,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState = makeVarState())(using Context): CaptureSet.CompareResult = try - if assocExistentials.isEmpty then + if assocExistentialsOLD.isEmpty then refs1.subCaptures(refs2, vs) else - val mapped = refs1.map(MapExistentials(assocExistentials)) - if mapped.elems.exists(Existential.isBadExistential) + val mapped = refs1.map(MapExistentials(assocExistentialsOLD)) + if mapped.elems.exists(Existential.isBadExistentialOLD) then CaptureSet.CompareResult.Fail(refs2 :: Nil) else mapped.subCaptures(refs2, vs) catch case ex: AssertionError => @@ -3507,7 +3516,7 @@ object TypeComparer { def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = comparing(_.subCaptures(refs1, refs2, vs)) - def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = + def subsumesExistentiallyOLD(tp1: TermParamRef, tp2: CaptureRef)(using Context) = comparing(_.subsumesExistentially(tp1, tp2)) def isOpenedExistential(ref: CaptureRef)(using Context) = diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index cdfc844e5cba..15161e9afe98 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -48,6 +48,13 @@ class PlainPrinter(_ctx: Context) extends Printer { limiter.register(str) Texts.Str(str, lineRange) + private var openMethods: List[MethodOrPoly] = Nil + + protected def inOpenMethod[T](mt: MethodOrPoly | Null)(op: => T)(using Context): T = + val saved = openMethods + if mt != null then openMethods = mt :: openMethods + try op finally openMethods = saved + given stringToText: Conversion[String, Text] = Str(_) /** If true, tweak output so it is the same before and after pickling */ @@ -441,7 +448,13 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" - case Existential.Var(bv) => toTextRef(bv) + case Existential.VarOLD(bv) => toTextRef(bv) + case Existential.Vble(binder) => + // TODO: Better printing? USe a mode where we print more detailed + val vbleStr = openMethods.indexOf(binder) match + case -1 => "unknown.localcap" + case n => "outer_" * n ++ "localcap" + vbleStr ~ hashStr(binder) case Fresh(hidden) => val idStr = if showUniqueIds then s"#${hidden.id}" else "" if printFreshDetailed then s"" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index c0c5c43d21f7..68ba4a26952e 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -180,31 +180,38 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { ~ ")" argStr ~ " " ~ arrow(isContextual, isPure) ~ refs ~ " " ~ argText(res) - protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = info match - case info: MethodType => - val isContextual = info.isImplicitMethod - val capturesRoot = refs == rootSetText - if cc.isCaptureCheckingOrSetup && info.allParamNamesSynthetic && !info.looksDependent then - // cc.Setup converts all functions to dependent functions. Undo that when printing. - toTextFunction(info.paramInfos, info.resType, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) - else - changePrec(GlobalPrec): - "(" - ~ paramsText(info) - ~ ") " - ~ arrow(isContextual, isPure && !capturesRoot) - ~ refs.provided(!capturesRoot) - ~ " " - ~ toTextMethodAsFunction(info.resultType, isPure) - case info: PolyType => - changePrec(GlobalPrec) { - "[" - ~ paramsText(info) - ~ "] => " - ~ toTextMethodAsFunction(info.resultType, isPure) - } - case _ => - toText(info) + protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = + def recur(tp: Type, enclInfo: MethodOrPoly | Null): Text = tp match + case tp: MethodType => + val isContextual = tp.isImplicitMethod + val capturesRoot = refs == rootSetText + if cc.isCaptureCheckingOrSetup + && tp.allParamNamesSynthetic && !tp.looksDependent + && !showUniqueIds && !printDebug + then + // cc.Setup converts all functions to dependent functions. Undo that when printing. + inOpenMethod(tp): + toTextFunction(tp.paramInfos, tp.resType, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) + else + changePrec(GlobalPrec): + "(" + ~ paramsText(tp) + ~ ") " + ~ arrow(isContextual, isPure && !capturesRoot) + ~ refs.provided(!capturesRoot) + ~ " " + ~ recur(tp.resultType, tp) + case tp: PolyType => + changePrec(GlobalPrec) { + "[" + ~ paramsText(tp) + ~ "] => " + ~ recur(tp.resultType, tp) + } + case _ => + inOpenMethod(enclInfo): + toText(tp) + recur(info, null) override def toText(tp: Type): Text = controlled { def toTextTuple(args: List[Type]): Text = diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 10048255918e..e9383f6e4c8c 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -387,12 +387,30 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz */ def lazyAppendedAll[B >: A](suffix: => collection.IterableOnce[B]^): LazyListIterable[B]^{this, suffix} = newLL { - if (isEmpty) suffix match { + {if (isEmpty) suffix match { case lazyList: LazyListIterable[B] => lazyList.state // don't recompute the LazyListIterable case coll if coll.knownSize == 0 => State.Empty case coll => stateFromIterator(coll.iterator) } else sCons(head, tail lazyAppendedAll suffix) + }.asInstanceOf + /* TODO: Without the asInstanceOf, we get + [error] 390 | {if (isEmpty) suffix match { + [error] | ^y-cc / Compile / compileIncremental 10s + [error] |Found: () ?->{suffix} + [error] | scala.collection.immutable.LazyListIterable.State[box B^?]^{unknown.localcap} + [error] |Required: () ?->{fresh} + [error] | scala.collection.immutable.LazyListIterable.State[box B^?]^{localcap} + [error] 391 | case lazyList: LazyListIterable[B] => lazyList.state // don't recompute the LazyListIterable + [error] 392 | case coll if coll.knownSize == 0 => State.Empty + [error] 393 | case coll => stateFromIterator(coll.iterator) + [error] 394 | } + [error] 395 | else sCons(head, tail lazyAppendedAll suffix) + [error] 396 | }//.asInstanceOf + [error] | + + Figure out why we found a result with capture {unknown.localcap}. + */ } /** @inheritdoc diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 979bb17e2031..ac768350533c 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -36,33 +36,33 @@ -- Error: tests/neg-custom-args/captures/capt1.scala:36:16 ------------------------------------------------------------- 36 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^ - | Type variable X of method h cannot be instantiated to () -> (ex$14: caps.Exists) -> C^{ex$14} since - | the part C^{ex$14} of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to () -> box C^{localcap} since + | the part box C^{unknown.localcap} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:36:30 ------------------------------------------------------------- 36 | val z2 = h[() -> Cap](() => x) // error // error | ^ - | reference (x : C^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> (ex$14: caps.Exists) -> C^{ex$14} + | reference (x : C^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> box C^{localcap} -- Error: tests/neg-custom-args/captures/capt1.scala:38:13 ------------------------------------------------------------- 38 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ - | Type variable X of method h cannot be instantiated to box () ->{x} (ex$19: caps.Exists) -> C^{ex$19} since - | the part C^{ex$19} of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to box () ->{x} C^{localcap} since + | the part C^{unknown.localcap} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:43:7 -------------------------------------------------------------- 43 | if x == null then // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => (ex$23: caps.Exists) -> C^{ex$23}. + | of value z1 with type () => C^{localcap}. | This type hides capabilities {x} -- Error: tests/neg-custom-args/captures/capt1.scala:44:12 ------------------------------------------------------------- 44 | () => x // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => (ex$23: caps.Exists) -> C^{ex$23}. + | of value z1 with type () => C^{localcap}. | This type hides capabilities {x} -- Error: tests/neg-custom-args/captures/capt1.scala:47:2 -------------------------------------------------------------- 47 | x // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => (ex$23: caps.Exists) -> C^{ex$23}. + | of value z1 with type () => C^{localcap}. | This type hides capabilities {x} diff --git a/tests/neg-custom-args/captures/existential-mapping.check b/tests/neg-custom-args/captures/existential-mapping.check index 6a0da29d7ce5..af37e94bc482 100644 --- a/tests/neg-custom-args/captures/existential-mapping.check +++ b/tests/neg-custom-args/captures/existential-mapping.check @@ -5,84 +5,84 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:9:25 --------------------------- 9 | val _: (x: C^) -> C = x1 // error | ^^ - | Found: (x1 : (x: C^) -> (ex$3: caps.Exists) -> C^{ex$3}) + | Found: (x1 : (x: C^) -> C^{localcap}) | Required: (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:12:20 -------------------------- 12 | val _: C^ -> C = x2 // error | ^^ - | Found: (x2 : C^ -> (ex$7: caps.Exists) -> C^{ex$7}) + | Found: (x2 : C^ -> C^{localcap}) | Required: C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:15:30 -------------------------- 15 | val _: A^ -> (x: C^) -> C = x3 // error | ^^ - | Found: (x3 : A^ -> (x: C^) -> (ex$9: caps.Exists) -> C^{ex$9}) + | Found: (x3 : A^ -> (x: C^) -> C^{localcap}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:18:25 -------------------------- 18 | val _: A^ -> C^ -> C = x4 // error | ^^ - | Found: (x4 : A^ -> C^ -> (ex$17: caps.Exists) -> C^{ex$17}) + | Found: (x4 : A^ -> C^ -> C^{localcap}) | Required: A^ -> C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:21:30 -------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (x: C^) -> (ex$23: caps.Exists) -> C^{ex$23}) + | Found: (x5 : A^ -> (x: C^) -> C^{localcap}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:24:30 -------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (ex$32: caps.Exists) -> (x: C^) ->{ex$32} (ex$31: caps.Exists) -> C^{ex$31}) - | Required: A^ -> (ex$35: caps.Exists) -> (x: C^) ->{ex$35} C + | Found: (x6 : A^ -> (x: C^) ->{localcap} C^{localcap}) + | Required: A^ -> (x: C^) ->{localcap} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:27:25 -------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) ->{fresh} (ex$37: caps.Exists) -> C^{ex$37}) + | Found: (y1 : (x: C^) ->{fresh} C^{localcap}) | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:30:20 -------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ ->{fresh} (ex$41: caps.Exists) -> C^{ex$41}) + | Found: (y2 : C^ ->{fresh} C^{localcap}) | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:33:30 -------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ ->{fresh} (ex$44: caps.Exists) -> (x: C^) ->{ex$44} (ex$43: caps.Exists) -> C^{ex$43}) - | Required: A^ ->{fresh} (ex$47: caps.Exists) -> (x: C^) ->{ex$47} C + | Found: (y3 : A^ ->{fresh} (x: C^) ->{localcap} C^{localcap}) + | Required: A^ ->{fresh} (x: C^) ->{localcap} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:36:25 -------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ ->{fresh} (ex$50: caps.Exists) -> C^ ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) - | Required: A^ ->{fresh} (ex$52: caps.Exists) -> C^ ->{ex$52} C + | Found: (y4 : A^ ->{fresh} C^ ->{localcap} C^{localcap}) + | Required: A^ ->{fresh} C^ ->{localcap} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:39:30 -------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ ->{fresh} (x: C^) -> (ex$54: caps.Exists) -> C^{ex$54}) + | Found: (y5 : A^ ->{fresh} (x: C^) -> C^{localcap}) | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:42:30 -------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ ->{fresh} (ex$63: caps.Exists) -> (x: C^) ->{ex$63} (ex$62: caps.Exists) -> C^{ex$62}) - | Required: A^ ->{fresh} (ex$66: caps.Exists) -> (x: C^) ->{ex$66} C + | Found: (y6 : A^ ->{fresh} (x: C^) ->{localcap} C^{localcap}) + | Required: A^ ->{fresh} (x: C^) ->{localcap} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.scala b/tests/neg-custom-args/captures/heal-tparam-cs.scala index fde4b93e196c..4abe014e9b43 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.scala +++ b/tests/neg-custom-args/captures/heal-tparam-cs.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +//import language.`3.8` trait Capp { def use(): Unit } @@ -11,12 +12,12 @@ def main(io: Capp^, net: Capp^): Unit = { } val test2: (c: Capp^) -> () => Unit = - localCap { c => // error + localCap { c => // ok (c1: Capp^) => () => { c1.use() } } val test3: (c: Capp^{io}) -> () ->{io} Unit = - localCap { c => // error + localCap { c => // ok (c1: Capp^{io}) => () => { c1.use() } } @@ -31,4 +32,14 @@ def main(io: Capp^, net: Capp^): Unit = { localCap2 { c => // ok () => { c.use() } } + } + +// Original issue from PR #16264 +def main2() = { + val f: (io: Capp^) -> () -> Unit = + io => () => io.use() // error + + val g: (Capp^) -> () -> Unit = + io => () => io.use() // error +} \ No newline at end of file diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index 7f59697cdd87..f04951095702 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -6,12 +6,12 @@ -- [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} + | Found: bar.T + | Required: () => Logger^ | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/i19330.scala:16:14 ------------------------------------------------------------ 16 | val t: () => Logger^ = () => l // error | ^^^^^^^^^^^^^ - | Separation failure: value t's type () => (ex$3: caps.Exists) -> Logger^{ex$3} hides parameter l. + | Separation failure: value t's type () => Logger^{localcap} hides parameter l. | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index 94ab4b94b57e..0a7a69635454 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -21,5 +21,5 @@ -- Error: tests/neg-custom-args/captures/i21401.scala:17:52 ------------------------------------------------------------ 17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error | ^^^^^^^^^^^^^^^^^^^^^^^^ - |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> (ex$14: caps.Exists) -> Boxed[box IO^{ex$14}] since - |the part box IO^{ex$14} of that type captures the root capability `cap`. + | Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> Boxed[box IO^{localcap}] since + | the part box IO^{unknown.localcap} of that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index c25417ed5454..d0db9d983832 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -8,10 +8,10 @@ -- [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$14: caps.Exists) -> box Logger{val f: File^{_$1}}^{ex$14.rd, _$1} - |Required: (_$1: box File^{files*}) => box Logger{val f: File^?}^? + | Found: (_$1: box File^{files*}) ->{files*} box Logger{val f: File^{_$1}}^{localcap.rd, _$1} + | Required: (_$1: box File^{files*}) ->{fresh} box Logger{val f: File^?}^? | - |Note that the universal capability `cap.rd` - |cannot be included in capture set ? + | Note that reference unknown.localcap.rd + | cannot be included in outer capture set ? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i21920.check b/tests/neg-custom-args/captures/i21920.check index 8efa24426d01..b022d71b8418 100644 --- a/tests/neg-custom-args/captures/i21920.check +++ b/tests/neg-custom-args/captures/i21920.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21920.scala:34:34 --------------------------------------- 34 | val cell: Cell[File] = File.open(f => Cell(Seq(f))) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: Cell[box File^{f, f²}]{val head: () ?->? IterableOnce[box File^{f, f²}]^?}^? + | Found: Cell[box File^{f, f²}]{val head: () ?->? IterableOnce[box File^{f²}]^?}^? | Required: Cell[File] | | where: f is a reference to a value parameter diff --git a/tests/neg-custom-args/captures/leaked-curried.check b/tests/neg-custom-args/captures/leaked-curried.check index be11aedd74ae..f043d4067947 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -1,10 +1,10 @@ -- Error: tests/neg-custom-args/captures/leaked-curried.scala:14:20 ---------------------------------------------------- 14 | () => () => io // error | ^^ - | reference (io : Cap^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> () ->{io} (ex$7: caps.Exists) -> Cap^{ex$7} + | reference (io : Cap^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> () ->{io} Cap^{localcap} -- Error: tests/neg-custom-args/captures/leaked-curried.scala:17:20 ---------------------------------------------------- 17 | () => () => io // error | ^^ - | reference (io : Cap^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> () ->{io} (ex$15: caps.Exists) -> Cap^{ex$15} + | reference (io : Cap^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> () ->{io} Cap^{localcap} diff --git a/tests/neg-custom-args/captures/sep-use2.check b/tests/neg-custom-args/captures/sep-use2.check new file mode 100644 index 000000000000..25ad63646fd3 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use2.check @@ -0,0 +1,51 @@ +-- Error: tests/neg-custom-args/captures/sep-use2.scala:5:10 ----------------------------------------------------------- +5 | def cc: Object^ = c // error + | ^^^^^^^ + | Separation failure: method cc's result type Object^ hides non-local parameter c +-- Error: tests/neg-custom-args/captures/sep-use2.scala:13:8 ----------------------------------------------------------- +13 | { f(c) } // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of method cc with result type Object^. + | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:12:10 ---------------------------------------------------------- +12 | val x4: Object^ = // error + | ^^^^^^^ + | Separation failure: value x4's type Object^ hides parameter f. + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sep-use2.scala:16:10 ---------------------------------------------------------- +16 | def cc: Object^ = c // error + | ^^^^^^^ + | Separation failure: method cc's result type Object^ hides non-local parameter c +-- Error: tests/neg-custom-args/captures/sep-use2.scala:18:6 ----------------------------------------------------------- +18 | { f(cc) } // error // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of method cc with result type Object^. + | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:18:8 ----------------------------------------------------------- +18 | { f(cc) } // error // error + | ^^ + | Separation failure: argument of type (cc : -> Object^) + | to a function of type Object^ ->{c} Object^{localcap} + | corresponds to capture-polymorphic formal parameter x$0 of type Object^ + | and hides capabilities {cap, c}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {cap, c} + | Hidden footprint of current argument : {c} + | Capture set of function prefix : {f} + | Footprint set of function prefix : {f, c} + | The two sets overlap at : {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:20:6 ----------------------------------------------------------- +20 | { f(c) } // error // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of method cc with result type Object^. + | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:20:8 ----------------------------------------------------------- +20 | { f(c) } // error // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of method cc with result type Object^. + | This type hides capabilities {c} diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 23c1b056c659..b67dc464d929 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -3,11 +3,13 @@ | ^^^^^^^^^^^^^^^^^^^ | Type variable R of method handle cannot be instantiated to box CT[Exception]^ since | that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:30:32 ------------------------------------------ 30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error - | ^ - | reference (x : CT[Exception]^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () ->? Nothing + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: () ->{x} Nothing + | Required: () ->? Nothing + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:52:2 ------------------------------------------- 47 |val global: () -> Int = handle { 48 | (x: CanThrow[Exception]) => @@ -16,11 +18,8 @@ 51 | 22 52 |} { // error | ^ - | Found: () ->{x, x²} Int + | Found: () ->{x} Int | Required: () -> Int - | - | where: x is a reference to a value parameter - | x² is a reference to a value parameter 53 | (ex: Exception) => () => 22 54 |} | diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index c02d95904e13..65efbf1a08b9 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -14,6 +14,6 @@ 17 | def use(@consume x: F): File^ = x // error @consume override | ^ |error overriding method use in trait Foo of type (x: File^): box File^; - | method use of type (x: File^): (ex$2: caps.Exists) -> File^{ex$2} has a parameter x with different @consume status than the corresponding parameter in the overridden definition + | method use of type (x: File^): File^{unknown.localcap} has a parameter x with different @consume status than the corresponding parameter in the overridden definition | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index a05eefd16622..bd5e017a2b0c 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,10 +1,13 @@ --- Error: tests/neg-custom-args/captures/vars.scala:24:14 -------------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:24:8 ------------------------------------------ 24 | a = x => g(x) // error - | ^^^^ - | reference (cap3 : CC^) is not included in the allowed capture set {cap1} of variable a + | ^^^^^^^^^ + | Found: (x: String) ->{cap3} String + | Required: (x: String) ->{cap1} String + | + | Note that reference (cap3 : CC^), defined in method scope + | cannot be included in outer capture set {cap1} of variable a | - | Note that reference (cap3 : CC^), defined in method scope - | cannot be included in outer capture set {cap1} of variable a + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:8 ------------------------------------------ 25 | a = g // error | ^ From 449d409454814f2bc8de8dd01db335a56d1e7557 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Feb 2025 13:55:05 +0100 Subject: [PATCH 55/93] Improve printing of existential Fresh instances Print them as - cap if bound to the immediately enclosing method - (outer_)*cap if bound to some outer enclosing method - if bound to some other method type MT Sets that only consist of a reference that prints as `cap` are elided by printing just `^` or `=>`. --- .../tools/dotc/printing/PlainPrinter.scala | 29 ++++++++++++----- tests/neg-custom-args/captures/capt1.check | 16 +++++----- .../captures/existential-mapping.check | 32 +++++++++---------- tests/neg-custom-args/captures/i19330.check | 2 +- tests/neg-custom-args/captures/i21401.check | 4 +-- tests/neg-custom-args/captures/i21614.check | 4 +-- .../captures/leaked-curried.check | 4 +-- tests/neg-custom-args/captures/sep-use2.check | 2 +- .../captures/unsound-reach-4.check | 2 +- 9 files changed, 54 insertions(+), 41 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 15161e9afe98..90260a528ebc 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -52,7 +52,8 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def inOpenMethod[T](mt: MethodOrPoly | Null)(op: => T)(using Context): T = val saved = openMethods - if mt != null then openMethods = mt :: openMethods + if mt != null && !mt.resType.isInstanceOf[MethodOrPoly] then + openMethods = mt :: openMethods try op finally openMethods = saved given stringToText: Conversion[String, Text] = Str(_) @@ -262,9 +263,20 @@ class PlainPrinter(_ctx: Context) extends Printer { then toText(parent) else + // The set if universal if it consists only of caps.cap or + // only of an existential Fresh that is bound to the immediately enclosing method. + def isUniversal = + refs.elems.size == 1 + && (refs.isUniversal + || refs.elems.nth(0).match + case Existential.Vble(binder) => + openMethods.nonEmpty && openMethods.head == binder + case _ => + false + ) val refsText = - if refs.isUniversal then - if refs.elems.size == 1 then rootSetText else toTextCaptureSet(refs) + if isUniversal then + rootSetText else if !refs.elems.isEmpty && refs.elems.forall(_.isCapOrFresh) && !printFresh then rootSetText else @@ -295,7 +307,7 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ paramsText(tp) ~ ")" ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) - ~ toText(tp.resultType) + ~ inOpenMethod(tp)(toText(tp.resultType)) } case ExprType(restp) => def arrowText: Text = restp match @@ -451,10 +463,11 @@ class PlainPrinter(_ctx: Context) extends Printer { case Existential.VarOLD(bv) => toTextRef(bv) case Existential.Vble(binder) => // TODO: Better printing? USe a mode where we print more detailed - val vbleStr = openMethods.indexOf(binder) match - case -1 => "unknown.localcap" - case n => "outer_" * n ++ "localcap" - vbleStr ~ hashStr(binder) + val vbleText: Text = openMethods.indexOf(binder) match + case -1 => + "" + case n => "outer_" * n ++ "cap" + vbleText ~ hashStr(binder) case Fresh(hidden) => val idStr = if showUniqueIds then s"#${hidden.id}" else "" if printFreshDetailed then s"" diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index ac768350533c..691ed6a01e05 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -36,33 +36,33 @@ -- Error: tests/neg-custom-args/captures/capt1.scala:36:16 ------------------------------------------------------------- 36 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^ - | Type variable X of method h cannot be instantiated to () -> box C^{localcap} since - | the part box C^{unknown.localcap} of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to () -> box C^ since + | the part box C^{} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:36:30 ------------------------------------------------------------- 36 | val z2 = h[() -> Cap](() => x) // error // error | ^ | reference (x : C^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> box C^{localcap} + | of an enclosing function literal with expected type () -> box C^ -- Error: tests/neg-custom-args/captures/capt1.scala:38:13 ------------------------------------------------------------- 38 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ - | Type variable X of method h cannot be instantiated to box () ->{x} C^{localcap} since - | the part C^{unknown.localcap} of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to box () ->{x} C^ since + | the part C^{} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:43:7 -------------------------------------------------------------- 43 | if x == null then // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => C^{localcap}. + | of value z1 with type () => C^. | This type hides capabilities {x} -- Error: tests/neg-custom-args/captures/capt1.scala:44:12 ------------------------------------------------------------- 44 | () => x // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => C^{localcap}. + | of value z1 with type () => C^. | This type hides capabilities {x} -- Error: tests/neg-custom-args/captures/capt1.scala:47:2 -------------------------------------------------------------- 47 | x // error: separation | ^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z1 with type () => C^{localcap}. + | of value z1 with type () => C^. | This type hides capabilities {x} diff --git a/tests/neg-custom-args/captures/existential-mapping.check b/tests/neg-custom-args/captures/existential-mapping.check index af37e94bc482..3ed5f0808b9f 100644 --- a/tests/neg-custom-args/captures/existential-mapping.check +++ b/tests/neg-custom-args/captures/existential-mapping.check @@ -5,84 +5,84 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:9:25 --------------------------- 9 | val _: (x: C^) -> C = x1 // error | ^^ - | Found: (x1 : (x: C^) -> C^{localcap}) + | Found: (x1 : (x: C^) -> C^) | Required: (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:12:20 -------------------------- 12 | val _: C^ -> C = x2 // error | ^^ - | Found: (x2 : C^ -> C^{localcap}) + | Found: (x2 : C^ -> C^) | Required: C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:15:30 -------------------------- 15 | val _: A^ -> (x: C^) -> C = x3 // error | ^^ - | Found: (x3 : A^ -> (x: C^) -> C^{localcap}) + | Found: (x3 : A^ -> (x: C^) -> C^) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:18:25 -------------------------- 18 | val _: A^ -> C^ -> C = x4 // error | ^^ - | Found: (x4 : A^ -> C^ -> C^{localcap}) + | Found: (x4 : A^ -> C^ -> C^) | Required: A^ -> C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:21:30 -------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (x: C^) -> C^{localcap}) + | Found: (x5 : A^ -> (x: C^) -> C^) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:24:30 -------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (x: C^) ->{localcap} C^{localcap}) - | Required: A^ -> (x: C^) ->{localcap} C + | Found: (x6 : A^ -> (x: C^) => C^) + | Required: A^ -> (x: C^) => C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:27:25 -------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) ->{fresh} C^{localcap}) + | Found: (y1 : (x: C^) ->{fresh} C^) | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:30:20 -------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ ->{fresh} C^{localcap}) + | Found: (y2 : C^ ->{fresh} C^) | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:33:30 -------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ ->{fresh} (x: C^) ->{localcap} C^{localcap}) - | Required: A^ ->{fresh} (x: C^) ->{localcap} C + | Found: (y3 : A^ ->{fresh} (x: C^) => C^) + | Required: A^ ->{fresh} (x: C^) => C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:36:25 -------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ ->{fresh} C^ ->{localcap} C^{localcap}) - | Required: A^ ->{fresh} C^ ->{localcap} C + | Found: (y4 : A^ ->{fresh} C^ => C^) + | Required: A^ ->{fresh} C^ => C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:39:30 -------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ ->{fresh} (x: C^) -> C^{localcap}) + | Found: (y5 : A^ ->{fresh} (x: C^) -> C^) | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:42:30 -------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ ->{fresh} (x: C^) ->{localcap} C^{localcap}) - | Required: A^ ->{fresh} (x: C^) ->{localcap} C + | Found: (y6 : A^ ->{fresh} (x: C^) => C^) + | Required: A^ ->{fresh} (x: C^) => C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index f04951095702..ffe6643244af 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -13,5 +13,5 @@ -- Error: tests/neg-custom-args/captures/i19330.scala:16:14 ------------------------------------------------------------ 16 | val t: () => Logger^ = () => l // error | ^^^^^^^^^^^^^ - | Separation failure: value t's type () => Logger^{localcap} hides parameter l. + | Separation failure: value t's type () => Logger^ hides parameter l. | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index 0a7a69635454..d462522ab0b1 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -21,5 +21,5 @@ -- Error: tests/neg-custom-args/captures/i21401.scala:17:52 ------------------------------------------------------------ 17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error | ^^^^^^^^^^^^^^^^^^^^^^^^ - | Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> Boxed[box IO^{localcap}] since - | the part box IO^{unknown.localcap} of that type captures the root capability `cap`. + |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> Boxed[box IO^] since + |the part box IO^{} of that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index d0db9d983832..0786809d7c07 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -8,10 +8,10 @@ -- [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*} box Logger{val f: File^{_$1}}^{localcap.rd, _$1} + | Found: (_$1: box File^{files*}) ->{files*} box Logger{val f: File^{_$1}}^{cap.rd, _$1} | Required: (_$1: box File^{files*}) ->{fresh} box Logger{val f: File^?}^? | - | Note that reference unknown.localcap.rd + | Note that reference .rd | cannot be included in outer capture set ? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/leaked-curried.check b/tests/neg-custom-args/captures/leaked-curried.check index f043d4067947..23726db62508 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -2,9 +2,9 @@ 14 | () => () => io // error | ^^ | reference (io : Cap^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> () ->{io} Cap^{localcap} + | of an enclosing function literal with expected type () -> () ->{io} Cap^ -- Error: tests/neg-custom-args/captures/leaked-curried.scala:17:20 ---------------------------------------------------- 17 | () => () => io // error | ^^ | reference (io : Cap^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> () ->{io} Cap^{localcap} + | of an enclosing function literal with expected type () -> () ->{io} Cap^ diff --git a/tests/neg-custom-args/captures/sep-use2.check b/tests/neg-custom-args/captures/sep-use2.check index 25ad63646fd3..e7f5383a497f 100644 --- a/tests/neg-custom-args/captures/sep-use2.check +++ b/tests/neg-custom-args/captures/sep-use2.check @@ -27,7 +27,7 @@ 18 | { f(cc) } // error // error | ^^ | Separation failure: argument of type (cc : -> Object^) - | to a function of type Object^ ->{c} Object^{localcap} + | to a function of type Object^ ->{c} Object^ | corresponds to capture-polymorphic formal parameter x$0 of type Object^ | and hides capabilities {cap, c}. | Some of these overlap with the captures of the function prefix. diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index 65efbf1a08b9..c7323b5aeda9 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -14,6 +14,6 @@ 17 | def use(@consume x: F): File^ = x // error @consume override | ^ |error overriding method use in trait Foo of type (x: File^): box File^; - | method use of type (x: File^): File^{unknown.localcap} has a parameter x with different @consume status than the corresponding parameter in the overridden definition + | method use of type (x: File^): File^ has a parameter x with different @consume status than the corresponding parameter in the overridden definition | | longer explanation available when compiling with `-explain` From 2b264cf2b0ad44b7d9d39090bd4f6e43caf336ee Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Feb 2025 15:45:28 +0100 Subject: [PATCH 56/93] Improve printing of Fresh in parts of types We need to keep track of the enclosing type structure with all Fresh binders and restore it at the point where we are printing a part of a type. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 14 ++++++++++++ .../dotty/tools/dotc/cc/CheckCaptures.scala | 22 ++++++++++++++++++- .../src/dotty/tools/dotc/cc/Existential.scala | 4 ++-- .../dotty/tools/dotc/core/TypeComparer.scala | 9 ++------ .../tools/dotc/printing/PlainPrinter.scala | 16 +++++--------- .../tools/dotc/printing/RefinedPrinter.scala | 10 ++++----- tests/neg-custom-args/captures/capt1.check | 4 ++-- tests/neg-custom-args/captures/i21401.check | 4 ++-- 8 files changed, 53 insertions(+), 30 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f00dea192ab7..49df47023e34 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -101,6 +101,8 @@ class CCState: private var curLevel: Level = outermostLevel private val symLevel: mutable.Map[Symbol, Int] = mutable.Map() + private var openedFreshBinders: List[MethodType] = Nil + object CCState: opaque type Level = Int @@ -126,6 +128,14 @@ object CCState: if !p then ccs.curLevel = ccs.curLevel.nextInner try op finally ccs.curLevel = saved + inline def inOpenedFreshBinder[T](mt: MethodType)(op: => T)(using Context): T = + val ccs = ccState + val saved = ccs.openedFreshBinders + if mt.isFreshBinder then ccs.openedFreshBinders = mt :: ccs.openedFreshBinders + try op finally ccs.openedFreshBinders = saved + + def openedFreshBinders(using Context): List[MethodType] = ccState.openedFreshBinders + extension (x: Level) def isDefined: Boolean = x >= 0 def <= (y: Level) = (x: Int) <= y @@ -578,6 +588,10 @@ extension (tp: Type) case tp: MethodOrPoly => tp.resType.hasSuffix(other) case _ => false + def isFreshBinder(using Context): Boolean = tp match + case tp: MethodType => !tp.resType.isInstanceOf[MethodOrPoly] + case _ => false + extension (cls: ClassSymbol) def pureBaseClass(using Context): Option[Symbol] = diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index d94152d4920a..5e99c8678c86 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -166,6 +166,7 @@ object CheckCaptures: val check = new TypeTraverser: private val seen = new EqHashSet[TypeRef] + var openFreshBinders: List[MethodType] = Nil def traverse(t: Type) = t.dealiasKeepAnnots match @@ -184,13 +185,32 @@ object CheckCaptures: () case CapturingType(parent, refs) => if variance >= 0 then + val openBinders = openFreshBinders refs.disallowRootCapability: () => - def part = if t eq tp then "" else i"the part $t of " + def part = + if t eq tp then "" + else + // Show in context of all enclosing traversed fresh binders. + def showInOpenedFreshBinders(mts: List[MethodType]): String = mts match + case Nil => i"the part $t of " + case mt :: mts1 => + CCState.inOpenedFreshBinder(mt): + showInOpenedFreshBinders(mts1) + showInOpenedFreshBinders(openBinders.reverse) report.error( em"""$what cannot $have $tp since |${part}that type captures the root capability `cap`.$addendum""", pos) traverse(parent) + case defn.RefinedFunctionOf(mt) => + traverse(mt) + case t: MethodType if t.isFreshBinder => + atVariance(-variance): + t.paramInfos.foreach(traverse) + val saved = openFreshBinders + openFreshBinders = t :: openFreshBinders + try traverse(t.resType) + finally openFreshBinders = saved case t => traverseChildren(t) if ccConfig.useSealed then check.traverse(tp) diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 2fa9233819df..a76e9a0e02d0 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -311,7 +311,7 @@ object Existential: case t: MethodType => // skip parameters val saved = localBinders - if !t.resType.isInstanceOf[MethodOrPoly] && !deep then localBinders = localBinders + t + if t.isFreshBinder && !deep then localBinders = localBinders + t try t.derivedLambdaType(resType = this(t.resType)) finally localBinders = saved case t: PolyType => @@ -484,7 +484,7 @@ object Existential: val mt1 = apply(mt) if mt1 ne mt then mt1.toFunctionType(alwaysDependent = true) else t - case t: MethodType if variance > 0 && !t.resType.isInstanceOf[MethodOrPoly] => + case t: MethodType if variance > 0 && t.isFreshBinder => val t1 = mapOver(t).asInstanceOf[MethodType] t1.derivedLambdaType(resType = if ccConfig.newScheme then mapCap(t1.resType, t1, fail) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 04e9383d20ef..137c809cfbd8 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -823,7 +823,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling (tp1.signature consistentParams tp2.signature) && matchingMethodParams(tp1, tp2) && (!tp2.isImplicitMethod || tp1.isImplicitMethod) && - inOpenedMethod(tp2): + CCState.inOpenedFreshBinder(tp2): isSubType(tp1.resultType, tp2.resultType.subst(tp2, tp1)) case _ => false } @@ -2829,11 +2829,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling finally assocExistentialsOLD = saved - private def inOpenedMethod[T](mt: MethodType)(op: => T)(using Context): T = - val saved = openedMethods - if !mt.resType.isInstanceOf[MethodOrPoly] then openedMethods = mt :: openedMethods - try op finally openedMethods = saved - /** Is `tp1` an existential var that subsumes `tp2`? This is the case if `tp1` is * instantiatable (i.e. it's a key in `assocExistentials`) and one of the * following is true: @@ -2865,7 +2860,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def isOpenedExistential(ref: CaptureRef)(using Context): Boolean = ref match - case Existential.Vble(mt) => openedMethods.contains(mt) + case Existential.Vble(mt) => CCState.openedFreshBinders.contains(mt) case _ => openedExistentialsOLD.contains(ref) /** bi-map taking existentials to the left of a comparison to matching diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 90260a528ebc..ade192b991bd 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -48,14 +48,6 @@ class PlainPrinter(_ctx: Context) extends Printer { limiter.register(str) Texts.Str(str, lineRange) - private var openMethods: List[MethodOrPoly] = Nil - - protected def inOpenMethod[T](mt: MethodOrPoly | Null)(op: => T)(using Context): T = - val saved = openMethods - if mt != null && !mt.resType.isInstanceOf[MethodOrPoly] then - openMethods = mt :: openMethods - try op finally openMethods = saved - given stringToText: Conversion[String, Text] = Str(_) /** If true, tweak output so it is the same before and after pickling */ @@ -270,7 +262,9 @@ class PlainPrinter(_ctx: Context) extends Printer { && (refs.isUniversal || refs.elems.nth(0).match case Existential.Vble(binder) => - openMethods.nonEmpty && openMethods.head == binder + CCState.openedFreshBinders match + case b :: _ => binder eq b + case _ => false case _ => false ) @@ -307,7 +301,7 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ paramsText(tp) ~ ")" ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) - ~ inOpenMethod(tp)(toText(tp.resultType)) + ~ CCState.inOpenedFreshBinder(tp)(toText(tp.resultType)) } case ExprType(restp) => def arrowText: Text = restp match @@ -463,7 +457,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case Existential.VarOLD(bv) => toTextRef(bv) case Existential.Vble(binder) => // TODO: Better printing? USe a mode where we print more detailed - val vbleText: Text = openMethods.indexOf(binder) match + val vbleText: Text = CCState.openedFreshBinders.indexOf(binder) match case -1 => "" case n => "outer_" * n ++ "cap" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 68ba4a26952e..0e027508613b 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -181,7 +181,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { argStr ~ " " ~ arrow(isContextual, isPure) ~ refs ~ " " ~ argText(res) protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = - def recur(tp: Type, enclInfo: MethodOrPoly | Null): Text = tp match + def recur(tp: Type, enclInfo: MethodType | Null): Text = tp match case tp: MethodType => val isContextual = tp.isImplicitMethod val capturesRoot = refs == rootSetText @@ -190,7 +190,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { && !showUniqueIds && !printDebug then // cc.Setup converts all functions to dependent functions. Undo that when printing. - inOpenMethod(tp): + CCState.inOpenedFreshBinder(tp): toTextFunction(tp.paramInfos, tp.resType, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) else changePrec(GlobalPrec): @@ -206,11 +206,11 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "[" ~ paramsText(tp) ~ "] => " - ~ recur(tp.resultType, tp) + ~ recur(tp.resultType, enclInfo) } case _ => - inOpenMethod(enclInfo): - toText(tp) + if enclInfo != null then CCState.inOpenedFreshBinder(enclInfo)(toText(tp)) + else toText(tp) recur(info, null) override def toText(tp: Type): Text = controlled { diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 691ed6a01e05..804e18072752 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -37,7 +37,7 @@ 36 | 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`. + | the part box C^ of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:36:30 ------------------------------------------------------------- 36 | val z2 = h[() -> Cap](() => x) // error // error | ^ @@ -47,7 +47,7 @@ 38 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ | Type variable X of method h cannot be instantiated to box () ->{x} C^ since - | the part C^{} of that type captures the root capability `cap`. + | the part C^ of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:43:7 -------------------------------------------------------------- 43 | if x == null then // error: separation | ^ diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index d462522ab0b1..cb1400ebc420 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -21,5 +21,5 @@ -- Error: tests/neg-custom-args/captures/i21401.scala:17:52 ------------------------------------------------------------ 17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error | ^^^^^^^^^^^^^^^^^^^^^^^^ - |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> Boxed[box IO^] since - |the part box IO^{} of that type captures the root capability `cap`. + | Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> Boxed[box IO^] since + | the part box IO^ of that type captures the root capability `cap`. From a10ce6fb6a1dada7c90947859638a2b92a08d7b3 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Feb 2025 15:52:22 +0100 Subject: [PATCH 57/93] Drop old existential handling --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 8 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 3 - .../src/dotty/tools/dotc/cc/CaptureSet.scala | 15 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 19 +- .../src/dotty/tools/dotc/cc/Existential.scala | 190 +----------------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 - .../dotty/tools/dotc/core/Definitions.scala | 9 +- .../dotty/tools/dotc/core/TypeComparer.scala | 132 +----------- .../tools/dotc/printing/PlainPrinter.scala | 1 - .../tools/dotc/printing/RefinedPrinter.scala | 3 - 10 files changed, 12 insertions(+), 372 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 49df47023e34..d7796b6f5295 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -204,8 +204,7 @@ extension (tp: Type) * - annotated types that represent reach or maybe capabilities */ final def isTrackableRef(using Context): Boolean = tp match - case _: ThisType => true - case tp: TermParamRef => !Existential.isBinderOLD(tp) + case _: (ThisType | TermParamRef) => true case tp: TermRef => ((tp.prefix eq NoPrefix) || tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef @@ -215,7 +214,6 @@ extension (tp: Type) tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet) case tp: TypeParamRef => tp.derivesFrom(defn.Caps_CapSet) - case Existential.VarOLD(_) => true case Existential.Vble(_) => true case AnnotatedType(parent, annot) => defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef @@ -542,8 +540,6 @@ extension (tp: Type) // Also map existentials in results to reach capabilities if all // preceding arguments are known to be always pure t.derivedFunctionOrMethod(args, apply(Existential.toCap(res))) - case Existential(_, _) => - t case _ => mapOver(t) end narrowCaps @@ -571,8 +567,6 @@ extension (tp: Type) 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) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 00e38ece7832..786afa129066 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -6,7 +6,6 @@ import core.* import Types.*, Symbols.*, Contexts.*, Decorators.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.subsumesExistentiallyOLD import util.common.alwaysTrue import scala.collection.mutable import CCState.* @@ -110,7 +109,6 @@ 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 Existential.VarOLD(_) => true case Existential.Vble(_) => true case Fresh(_) => true case ReadOnlyCapability(tp1) => tp1.isMaxCapability @@ -229,7 +227,6 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false || this.match case ReachCapability(x1) => x1.subsumes(y.stripReach) - case Existential.VarOLD(bv) => subsumesExistentiallyOLD(bv, y) case x: TermRef => viaInfo(x.info)(subsumingRefs(_, y)) case x: TypeRef if assumedContainsOf(x).contains(y) => true case x: TypeRef if x.derivesFrom(defn.Caps_CapSet) => diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 64c29be1b582..0e5e4c39e5c4 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -104,7 +104,6 @@ sealed abstract class CaptureSet extends Showable: final def isUnboxable(using Context) = elems.exists: - case Existential.VarOLD(_) => true case Existential.Vble(_) => true case elem => elem.isRootCapability @@ -548,8 +547,6 @@ object CaptureSet: final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen addIfHiddenOrFail(elem) - else if Existential.isBadExistentialOLD(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 @@ -573,14 +570,9 @@ object CaptureSet: if elem.isRootCapability then !noUniversal else elem match - case Existential.VarOLD(bv) => - !noUniversal - && !TypeComparer.isOpenedExistential(bv) - // Opened existentials on the left cannot be added to nested capture sets on the right - // of a comparison. Test case is open-existential.scala. case elem @ Existential.Vble(mt) => !noUniversal - && !TypeComparer.isOpenedExistential(elem) + && !CCState.openedFreshBinders.contains(elem) // Opened existentials on the left cannot be added to nested capture sets on the right // of a comparison. Test case is open-existential.scala. case elem: TermRef if level.isDefined => @@ -636,7 +628,6 @@ object CaptureSet: try val approx = computeApprox(origin).ensuring(_.isConst) if approx.elems.exists: - case Existential.VarOLD(_) => true case Existential.Vble(_) => true case _ => false then @@ -1325,8 +1316,6 @@ object CaptureSet: case tp: (TypeRef | TypeParamRef) => if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet else empty - case tp @ Existential.VarOLD(_) => - tp.captureSet case tp @ Existential.Vble(_) => tp.captureSet case CapturingType(parent, refs) => @@ -1393,8 +1382,6 @@ object CaptureSet: case t @ FunctionOrMethod(args, res) => if args.forall(_.isAlwaysPure) then this(cs, Existential.toCap(res)) else cs - case t @ Existential(_, _) => - cs case _ => foldOver(cs, t) collect(CaptureSet.empty, tp) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 5e99c8678c86..944ca0712c73 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -23,7 +23,6 @@ import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} -import Existential.derivedExistentialTypeOLD import Annotations.Annotation /** The capture checker */ @@ -869,8 +868,6 @@ 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.derivedExistentialTypeOLD(augmentConstructorType(core1, initCs)) case _ => val (refined, cs) = addParamArgRefinements(core, initCs) refined.capturing(cs) @@ -942,8 +939,8 @@ class CheckCaptures extends Recheck, SymTransformer: // which are less intelligible. An example is the line `a = x` in // neg-custom-args/captures/vars.scala. That's why this code is conditioned. // to apply only to closures that are not eta expansions. - val res1 = Existential.toCapDeeply(res) // TODO: why toCapDeeply? - val pt1 = Existential.toCapDeeply(pt) + val res1 = Existential.toCap(res, deep = true) // TODO: why deep = true? + val pt1 = Existential.toCap(pt, deep = true) // We need to open existentials here in order not to get vars mixed up in them // We do the proper check with existentials when we are finished with the closure block. capt.println(i"pre-check closure $expr of type $res1 against $pt1") @@ -1450,17 +1447,9 @@ class CheckCaptures extends Recheck, SymTransformer: def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected" - // Get existentials and wildcards out of the way - actual match - case actual @ Existential(_, actualUnpacked) => - return Existential.derivedExistentialTypeOLD(actual): - recur(actualUnpacked, expected, covariant) - case _ => + // Get wildcards out of the way expected match - case expected @ Existential(_, expectedUnpacked) => - return recur(actual, expectedUnpacked, covariant) - case _: WildcardType => - return actual + case _: WildcardType => return actual case _ => trace(adaptStr, capt, show = true) { diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index a76e9a0e02d0..3282a215eb48 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -210,41 +210,6 @@ Expansion of ^: */ object Existential: - type CarrierOLD = RefinedType - - def unapply(tp: CarrierOLD)(using Context): Option[(TermParamRef, Type)] = - tp.refinedInfo match - case mt: MethodType - if isExistentialMethodOLD(mt) && defn.isNonRefinedFunction(tp.parent) => - Some(mt.paramRefs.head, mt.resultType) - case _ => None - - /** Create method type in the refinement of an existential type */ - private def exMethodTypeOLD(using Context)( - mk: TermParamRef => Type, - boundName: TermName = ExistentialBinderName.fresh() - ): MethodType = - MethodType(boundName :: Nil)( - mt => defn.Caps_Exists.typeRef :: Nil, - mt => mk(mt.paramRefs.head)) - - /** Create existential */ - def applyOLD(mk: TermParamRef => Type)(using Context): Type = - exMethodTypeOLD(mk).toFunctionType(alwaysDependent = true) - - /** The (super-) type of existentially bound references */ - type VarOLD = AnnotatedType - - /** An extractor for existentially bound references of the form ex @existential - * where ex is a TermParamRef of type Exists - */ - object VarOLD: - def apply(boundVar: TermParamRef)(using Context): VarOLD = - AnnotatedType(boundVar, Annotation(defn.ExistentialAnnot, NoSpan)) - def unapply(tp: VarOLD)(using Context): Option[TermParamRef] = tp match - case AnnotatedType(bv: TermParamRef, ann) if ann.symbol == defn.ExistentialAnnot => Some(bv) - case _ => None - /** The (super-) type of existentially bound references */ type Vble = AnnotatedType @@ -260,44 +225,6 @@ object Existential: case _ => None case _ => None - /** Create existential if bound variable appears in result of `mk` */ - def wrapOLD(mk: TermParamRef => Type)(using Context): Type = - val mt = exMethodTypeOLD(mk) - if mt.isResultDependent then mt.toFunctionType() else mt.resType - - extension (tp: CarrierOLD) - def derivedExistentialTypeOLD(core: Type)(using Context): Type = tp match - case Existential(boundVar, unpacked) => - if core eq unpacked then tp - else applyOLD(bv => core.substParam(boundVar, bv)) - case _ => - core - - /** Map existentially bound references referring to `boundVar` one-to-one - * to Fresh instances (OLD) - */ - def boundVarToCapOLD(boundVar: TermParamRef, tp: Type)(using Context) = - val subst = new IdempotentCaptRefMap: - val seen = EqHashMap[Annotation, CaptureRef]() - def apply(t: Type): Type = t match - case t @ VarOLD(`boundVar`) => - seen.getOrElseUpdate(t.annot, Fresh(NoSymbol)) - case _ => - mapOver(t) - subst(tp) - - /** Map existentially bound references referring to `boundVar` one-to-one - * to Fresh instances - */ - def toCapOLD(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match - case Existential(boundVar, unpacked) => // (OLD) - boundVarToCapOLD(boundVar, unpacked) - case tp1 @ CapturingType(parent, refs) => - tp1.derivedCapturingType(toCap(parent), refs) - case tp1 @ AnnotatedType(parent, ann) => - tp1.derivedAnnotatedType(toCap(parent), ann) - case _ => tp - /** Map top-level free existential variables one-to-one to Fresh instances */ def toCap(tp: Type, deep: Boolean = false)(using Context): Type = val subst = new IdempotentCaptRefMap: @@ -320,26 +247,8 @@ object Existential: case _ => mapOver(t) - if ccConfig.newScheme then subst(tp) - else toCapOLD(tp) - - /** Map existentials at the top-level and in all nested result types to `Fresh` - */ - def toCapDeeplyOLD(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match - case Existential(boundVar, unpacked) => - toCapDeeplyOLD(boundVarToCapOLD(boundVar, unpacked)) - case tp1 @ FunctionOrMethod(args, res) => - val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeplyOLD(res)) - if tp2 ne tp1 then tp2 else tp - case tp1 @ CapturingType(parent, refs) => - tp1.derivedCapturingType(toCapDeeplyOLD(parent), refs) - case tp1 @ AnnotatedType(parent, ann) => - tp1.derivedAnnotatedType(toCapDeeplyOLD(parent), ann) - case _ => tp - - def toCapDeeply(tp: Type)(using Context): Type = - if ccConfig.newScheme then toCap(tp, deep = true) - else toCapDeeplyOLD(tp) + subst(tp) + end toCap /** Knowing that `tp` is a function type, is it an alias to a function other * than `=>`? @@ -348,75 +257,6 @@ object Existential: case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) case _ => false - /** 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. - */ - def mapCapOLD(tp: Type, fail: Message => Unit)(using Context): Type = - var needsWrap = false - - abstract class CapMap extends BiTypeMap: - override def mapOver(t: Type): Type = t match - case t @ FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => - t // `t` should be mapped in this case by a different call to `mapCap`. - case Existential(_, _) => - t - case t: (LazyRef | TypeVar) => - mapConserveSuper(t) - case _ => - super.mapOver(t) - - class Wrap(boundVar: TermParamRef) extends CapMap: - private val seen = EqHashMap[CaptureRef, VarOLD]() - - def apply(t: Type) = t match - case t: CaptureRef if t.isCapOrFresh => - if variance > 0 then - needsWrap = true - seen.getOrElseUpdate(t, VarOLD(boundVar)) - else - if variance == 0 then - fail(em"""$tp captures the root capability `cap` in invariant position""") - // we accept variance < 0, and leave the cap as it is - super.mapOver(t) - case t @ CapturingType(parent, refs: CaptureSet.Var) => - if variance > 0 then needsWrap = true - super.mapOver(t) - case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => - if variance > 0 then - needsWrap = true - super.mapOver: - defn.FunctionNOf(args, res, contextual) - .capturing(VarOLD(boundVar).singletonCaptureSet) - else mapOver(t) - case _ => - mapOver(t) - //.showing(i"mapcap $t = $result") - - lazy val inverse = new BiTypeMap: - def apply(t: Type) = t match - case t @ VarOLD(`boundVar`) => - // do a reverse getOrElseUpdate on `seen` to produce the - // `Fresh` assosicated with `t` - val it = seen.iterator - var ref: CaptureRef | Null = null - while it.hasNext && ref == null do - val (k, v) = it.next - if v.annot eq t.annot then ref = k - if ref == null then - ref = Fresh(NoSymbol) - seen(ref) = t - ref - case _ => mapOver(t) - def inverse = Wrap.this - override def toString = "Wrap.inverse" - end Wrap - - val wrapped = applyOLD(Wrap(_)(tp)) - if needsWrap then wrapped else tp - end mapCapOLD - /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound * variable bound by `mt`. * Stop at function or method types since these have been mapped before. @@ -486,37 +326,13 @@ object Existential: else t case t: MethodType if variance > 0 && t.isFreshBinder => val t1 = mapOver(t).asInstanceOf[MethodType] - t1.derivedLambdaType(resType = - if ccConfig.newScheme then mapCap(t1.resType, t1, fail) - else mapCapOLD(t1.resType, fail)) + t1.derivedLambdaType(resType = mapCap(t1.resType, t1, fail)) case CapturingType(parent, refs) => t.derivedCapturingType(this(parent), refs) - case Existential(_, _) => - t case t: (LazyRef | TypeVar) => mapConserveSuper(t) case _ => mapFollowingAliases(t) end mapCapInResults - /** Is `mt` a method represnting an existential type when used in a refinement? */ - def isExistentialMethodOLD(mt: TermLambda)(using Context): Boolean = mt.paramInfos match - case (info: TypeRef) :: rest => info.symbol == defn.Caps_Exists && rest.isEmpty - case _ => false - - /** Is `ref` a TermParamRef representing existentially bound variables? */ - def isBinderOLD(ref: CaptureRef)(using Context) = ref match - case ref: TermParamRef => isExistentialMethodOLD(ref.binder) - case _ => false - - /** An value signalling an out-of-scope existential that should - * lead to a compare failure. - */ - def badExistentialOLD(using Context): TermParamRef = - exMethodTypeOLD(identity, nme.OOS_EXISTENTIAL).paramRefs.head - - def isBadExistentialOLD(ref: CaptureRef) = ref match - case ref: TermParamRef => ref.paramName == nme.OOS_EXISTENTIAL - case _ => false - end Existential diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index a368b6df920b..1402d566af7f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -297,9 +297,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case AnnotatedType(parent, annot) if annot.symbol.isRetains => // Drop explicit retains annotations apply(parent) - case Existential(_, unpacked) => - // drop the existential, the bound variables will be replaced by capture set variables - apply(unpacked) case tp: TypeLambda => // Don't recurse into parameter bounds, just cleanup any stray retains annotations tp.derivedLambdaType( @@ -904,7 +901,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val parent1 = this(parent) if refs.isUniversal then t.derivedCapturingType(parent1, CaptureSet.Fluid) else t - case Existential(_) => t case _ => mapFollowingAliases(t) /** Run setup on a compilation unit with given `tree`. diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index b4a243cc40b8..3dec2d2f8dda 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1225,13 +1225,8 @@ class Definitions { */ def unapply(tpe: RefinedType)(using Context): Option[MethodOrPoly] = tpe.refinedInfo match - case mt: MethodType - if tpe.refinedName == nme.apply - && isFunctionType(tpe.parent) - && !Existential.isExistentialMethodOLD(mt) => Some(mt) - case mt: PolyType - if tpe.refinedName == nme.apply - && isFunctionType(tpe.parent) => Some(mt) + case mt: MethodOrPoly + if tpe.refinedName == nme.apply && isFunctionType(tpe.parent) => Some(mt) case _ => None end RefinedFunctionOf diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 137c809cfbd8..3e9ad3f55a2c 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -47,8 +47,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling monitored = false GADTused = false opaquesUsed = false - openedExistentialsOLD = Nil - assocExistentialsOLD = Nil recCount = 0 needsGc = false if Config.checkTypeComparerReset then checkReset() @@ -67,19 +65,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** Indicates whether the subtype check used opaque types */ private var opaquesUsed: Boolean = false - /** In capture checking: The existential types that are open because they - * appear in an existential type on the left in an enclosing comparison. - */ - private var openedExistentialsOLD: List[TermParamRef] = Nil - private var openedMethods: List[MethodType] = Nil - - /** In capture checking: A map from existential types that are appear - * in an existential type on the right in an enclosing comparison. - * Each existential gets mapped to the opened existentials to which it - * may resolve at this point. - */ - private var assocExistentialsOLD: ExAssocOLD = Nil - private var myInstance: TypeComparer = this def currentInstance: TypeComparer = myInstance @@ -566,8 +551,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if reduced.exists then recur(reduced, tp2) && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } else thirdTry - case Existential(boundVar, tp1unpacked) => - compareExistentialLeft(boundVar, tp1unpacked, tp2) case _: FlexType => true case _ => @@ -652,8 +635,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling thirdTryNamed(tp2) case tp2: TypeParamRef => compareTypeParamRef(tp2) - case Existential(boundVar, tp2unpacked) => - compareExistentialRight(tp1, boundVar, tp2unpacked) case tp2: RefinedType => def compareRefinedSlow: Boolean = val name2 = tp2.refinedName @@ -2808,118 +2789,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // ----------- Capture checking ----------------------------------------------- - /** A type associating instantiatable existentials on the right of a comparison - * with the existentials they can be instantiated with. - */ - type ExAssocOLD = List[(TermParamRef, List[TermParamRef])] - - private def compareExistentialLeft(boundVar: TermParamRef, tp1unpacked: Type, tp2: Type)(using Context): Boolean = - val saved = openedExistentialsOLD - try - openedExistentialsOLD = boundVar :: openedExistentialsOLD - recur(tp1unpacked, tp2) - finally - openedExistentialsOLD = saved - - private def compareExistentialRight(tp1: Type, boundVar: TermParamRef, tp2unpacked: Type)(using Context): Boolean = - val saved = assocExistentialsOLD - try - assocExistentialsOLD = (boundVar, openedExistentialsOLD) :: assocExistentialsOLD - recur(tp1, tp2unpacked) - finally - assocExistentialsOLD = saved - - /** Is `tp1` an existential var that subsumes `tp2`? This is the case if `tp1` is - * instantiatable (i.e. it's a key in `assocExistentials`) and one of the - * following is true: - * - `tp2` is not an existential var, - * - `tp1` is associated via `assocExistentials` with `tp2`, - * - `tp2` appears as key in `assocExistentials` further out than `tp1`. - * The third condition allows to instantiate c2 to c1 in - * EX c1: A -> Ex c2. B - */ - def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean = - def canInstantiateWith(assoc: ExAssocOLD): Boolean = assoc match - case (bv, bvs) :: assoc1 => - if bv == tp1 then - tp2 match - case Existential.VarOLD(bv2) => - bvs.contains(bv2) || assoc1.exists(_._1 == bv2) - case _ => - true - else - canInstantiateWith(assoc1) - case Nil => - false - tp2 match - case Existential.VarOLD(bv2) if tp1 eq bv2 => - true // for now, existential references referring to the same - // binder are identified. !!! TODO this needs to be revised - case _ => - canInstantiateWith(assocExistentialsOLD) - - def isOpenedExistential(ref: CaptureRef)(using Context): Boolean = - ref match - case Existential.Vble(mt) => CCState.openedFreshBinders.contains(mt) - case _ => openedExistentialsOLD.contains(ref) - - /** bi-map taking existentials to the left of a comparison to matching - * existentials on the right. This is not a bijection. However - * we have `forwards(backwards(bv)) == bv` for an existentially bound `bv`. - * That's enough to qualify as a BiTypeMap. - */ - private class MapExistentials(assoc: ExAssocOLD)(using Context) extends BiTypeMap: - - private def bad(t: Type) = - Existential.badExistentialOLD - .showing(i"existential match not found for $t in $assoc", capt) - - def apply(t: Type) = t match - case t: TermParamRef if Existential.isBinderOLD(t) => - // Find outermost existential on the right that can be instantiated to `t`, - // or `badExistential` if none exists. - def findMapped(assoc: ExAssocOLD): CaptureRef = assoc match - case (bv, assocBvs) :: assoc1 => - val outer = findMapped(assoc1) - if !Existential.isBadExistentialOLD(outer) then outer - else if assocBvs.contains(t) then bv - else bad(t) - case Nil => - bad(t) - findMapped(assoc) - case _ => - mapOver(t) - - /** The inverse takes existentials on the right to the innermost existential - * on the left to which they can be instantiated. - */ - lazy val inverse = new BiTypeMap: - def apply(t: Type) = t match - case t: TermParamRef if Existential.isBinderOLD(t) => - assoc.find(_._1 == t) match - case Some((_, bvs)) if bvs.nonEmpty => bvs.head - case _ => bad(t) - case _ => - mapOver(t) - - def inverse = MapExistentials.this - override def toString = "MapExistentials.inverse" - end inverse - end MapExistentials - protected def makeVarState() = if frozenConstraint then CaptureSet.VarState.Closed() else CaptureSet.VarState() protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState = makeVarState())(using Context): CaptureSet.CompareResult = try - if assocExistentialsOLD.isEmpty then - refs1.subCaptures(refs2, vs) - else - val mapped = refs1.map(MapExistentials(assocExistentialsOLD)) - if mapped.elems.exists(Existential.isBadExistentialOLD) - then CaptureSet.CompareResult.Fail(refs2 :: Nil) - else mapped.subCaptures(refs2, vs) + refs1.subCaptures(refs2, vs) catch case ex: AssertionError => println(i"fail while subCaptures $refs1 <:< $refs2") throw ex @@ -3510,12 +3386,6 @@ object TypeComparer { def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = comparing(_.subCaptures(refs1, refs2, vs)) - - def subsumesExistentiallyOLD(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 ade192b991bd..d6cf8907ed23 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -454,7 +454,6 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" - case Existential.VarOLD(bv) => toTextRef(bv) case Existential.Vble(binder) => // TODO: Better printing? USe a mode where we print more detailed val vbleText: Text = CCState.openedFreshBinders.indexOf(binder) match diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 0e027508613b..32cbd4b07887 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -295,9 +295,6 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { if !printDebug && appliedText(tp.asInstanceOf[HKLambda].resType).isEmpty => // don't eta contract if the application would be printed specially toText(tycon) - case Existential(boundVar, unpacked) - if !printDebug && !ctx.settings.YccDebug.value && !unpacked.existsPart(_ == boundVar) => - toText(unpacked) case tp: RefinedType if defn.isFunctionType(tp) && !printDebug => toTextMethodAsFunction(tp.refinedInfo, isPure = Feature.pureFunsEnabled && !tp.typeSymbol.name.isImpureFunction, From fc8425bdb0ed696f0579f51438fa926f81146346 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Feb 2025 16:17:58 +0100 Subject: [PATCH 58/93] Drop deep toCap when checking closures It seems it's not necessary and it is problematic for soundness. --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 4 ++-- compiler/src/dotty/tools/dotc/cc/Existential.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 944ca0712c73..69cc650dbed5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -939,8 +939,8 @@ class CheckCaptures extends Recheck, SymTransformer: // which are less intelligible. An example is the line `a = x` in // neg-custom-args/captures/vars.scala. That's why this code is conditioned. // to apply only to closures that are not eta expansions. - val res1 = Existential.toCap(res, deep = true) // TODO: why deep = true? - val pt1 = Existential.toCap(pt, deep = true) + val res1 = Existential.toCap(res) // TODO: why deep = true? + val pt1 = Existential.toCap(pt) // We need to open existentials here in order not to get vars mixed up in them // We do the proper check with existentials when we are finished with the closure block. capt.println(i"pre-check closure $expr of type $res1 against $pt1") diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 3282a215eb48..1aaf1a8a249e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -226,7 +226,7 @@ object Existential: case _ => None /** Map top-level free existential variables one-to-one to Fresh instances */ - def toCap(tp: Type, deep: Boolean = false)(using Context): Type = + def toCap(tp: Type)(using Context): Type = val subst = new IdempotentCaptRefMap: val seen = EqHashMap[Annotation, CaptureRef]() var localBinders: SimpleIdentitySet[MethodType] = SimpleIdentitySet.empty @@ -238,7 +238,7 @@ object Existential: case t: MethodType => // skip parameters val saved = localBinders - if t.isFreshBinder && !deep then localBinders = localBinders + t + if t.isFreshBinder then localBinders = localBinders + t try t.derivedLambdaType(resType = this(t.resType)) finally localBinders = saved case t: PolyType => From 0e0388ef89c3ce2c514e02f211f195b394085392 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Feb 2025 16:24:07 +0100 Subject: [PATCH 59/93] Drop followResult = true in CaptureSet.ofInfo --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 0e5e4c39e5c4..366d1d2f426d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1303,9 +1303,15 @@ object CaptureSet: ref1.captureSetOfInfo.map(ReadOnlyMap()) case _ => if ref.isMaxCapability then ref.singletonCaptureSet - else ofType(ref.underlying, followResult = true) // TODO: why followResult = true? - - /** Capture set of a type */ + else ofType(ref.underlying, followResult = false) + + /** Capture set of a type + * @param followResult If true, also include capture sets of function results. + * This mode is currently not used. It could be interesting + * when we change the system so that the capture set of a function + * is the union of the capture sets if its span. + * In this case we should use `followResult = true` in the call in ofInfo above. + */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = def recur(tp: Type): CaptureSet = trace(i"ofType $tp, ${tp.getClass} $followResult", show = true): tp.dealiasKeepAnnots match From 60eb732c3bda762611fcad7ffc1b5f0d5dc59b5a Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Feb 2025 16:55:36 +0100 Subject: [PATCH 60/93] Make `inOpenedFreshBinder` work also if there is no capture checking phase --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index d7796b6f5295..f99f0c165ff8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -116,24 +116,37 @@ object CCState: */ def currentLevel(using Context): Level = ccState.curLevel + /** Perform `op` in the next inner level + * @pre We are currently in capture checking or setup + */ inline def inNestedLevel[T](inline op: T)(using Context): T = val ccs = ccState val saved = ccs.curLevel ccs.curLevel = ccs.curLevel.nextInner try op finally ccs.curLevel = saved + /** Perform `op` in the next inner level unless `p` holds. + * @pre We are currently in capture checking or setup + */ inline def inNestedLevelUnless[T](inline p: Boolean)(inline op: T)(using Context): T = val ccs = ccState val saved = ccs.curLevel if !p then ccs.curLevel = ccs.curLevel.nextInner try op finally ccs.curLevel = saved + /** If we are currently in capture checking or setup, perform `op` assuming + * a fresh enclosing binder `mt`, otherwise perform `op` directly. + */ inline def inOpenedFreshBinder[T](mt: MethodType)(op: => T)(using Context): T = - val ccs = ccState - val saved = ccs.openedFreshBinders - if mt.isFreshBinder then ccs.openedFreshBinders = mt :: ccs.openedFreshBinders - try op finally ccs.openedFreshBinders = saved - + if isCaptureCheckingOrSetup then + val ccs = ccState + val saved = ccs.openedFreshBinders + if mt.isFreshBinder then ccs.openedFreshBinders = mt :: ccs.openedFreshBinders + try op finally ccs.openedFreshBinders = saved + else + op + + /** The currently opened fresh binders */ def openedFreshBinders(using Context): List[MethodType] = ccState.openedFreshBinders extension (x: Level) @@ -147,7 +160,7 @@ object CCState: end CCState /** The currently valid CCState */ -def ccState(using Context) = +def ccState(using Context): CCState = Phases.checkCapturesPhase.asInstanceOf[CheckCaptures].ccState1 extension (tree: Tree) From 3c9545f53977aa416c27404c6c08cbe4e74b1aaa Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Feb 2025 18:42:24 +0100 Subject: [PATCH 61/93] Update check files --- tests/neg-custom-args/captures/depfun-reach.check | 12 ++++++------ tests/neg-custom-args/captures/depfun-reach.scala | 6 ++++++ tests/neg/polymorphic-erased-functions-types.check | 12 ++++++++++-- tests/neg/polymorphic-functions1.check | 5 ++++- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index bbe9347e5d6a..243b9fd56db7 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -1,19 +1,19 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:13:4 ---------------------------------- -13 | op // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:19:4 ---------------------------------- +19 | op // error | ^^ | Found: (xs: List[(X, box () ->{io} Unit)]) ->{op} List[box () ->{xs*} Unit] | Required: (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:20:60 --------------------------------- -20 | val b: (xs: List[() ->{io} Unit]) => List[() ->{} Unit] = a // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:26:60 --------------------------------- +26 | val b: (xs: List[() ->{io} Unit]) => List[() ->{} Unit] = a // error | ^ | Found: (xs: List[box () ->{io} Unit]) ->{a} List[box () ->{xs*} Unit] | Required: (xs: List[box () ->{io} Unit]) => List[() -> Unit] | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/depfun-reach.scala:12:17 ------------------------------------------------------ -12 | : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = // error +-- Error: tests/neg-custom-args/captures/depfun-reach.scala:18:17 ------------------------------------------------------ +18 | : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |Separation failure: method foo's result type (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] hides parameter op. |The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/depfun-reach.scala b/tests/neg-custom-args/captures/depfun-reach.scala index 4b2d662901b7..5e8c298df637 100644 --- a/tests/neg-custom-args/captures/depfun-reach.scala +++ b/tests/neg-custom-args/captures/depfun-reach.scala @@ -1,6 +1,12 @@ import language.experimental.captureChecking import caps.cap +trait List[+T]: + def foreach(op: T => Unit): Unit = ??? + +object List: + def apply[T](elem: T): List[T] = ??? + def test(io: Object^, async: Object^) = def compose(op: List[(() ->{cap} Unit, () ->{cap} Unit)]): List[() ->{op*} Unit] = List(() => op.foreach((f,g) => { f(); g() })) diff --git a/tests/neg/polymorphic-erased-functions-types.check b/tests/neg/polymorphic-erased-functions-types.check index 78a0ab37b1ab..f46940d61fa2 100644 --- a/tests/neg/polymorphic-erased-functions-types.check +++ b/tests/neg/polymorphic-erased-functions-types.check @@ -9,7 +9,10 @@ 4 |def t1b: [T] => (erased t: T) => Unit = [T] => (t: T) => () // error | ^^^^^^^^^^^^^^^^^^^ | Found: [T] => (t: T) => Unit - | Required: [T] => (erased t: T) => Unit + | Required: [T] => (erased t²: T) => Unit + | + | where: t is a reference to a value parameter + | t² is a reference to a value parameter | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/polymorphic-erased-functions-types.scala:6:36 --------------------------------- @@ -23,6 +26,11 @@ 7 |def t2b: [T, U] => (t: T, erased u: U) => Unit = [T, U] => (t: T, u: U) => () // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Found: [T, U] => (t: T, u: U) => Unit - | Required: [T, U] => (t: T, erased u: U) => Unit + | Required: [T, U] => (t²: T, erased u²: U) => Unit + | + | where: t is a reference to a value parameter + | t² is a reference to a value parameter + | u is a reference to a value parameter + | u² is a reference to a value parameter | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/polymorphic-functions1.check b/tests/neg/polymorphic-functions1.check index eef268c298cf..f925fe7b0f52 100644 --- a/tests/neg/polymorphic-functions1.check +++ b/tests/neg/polymorphic-functions1.check @@ -2,6 +2,9 @@ 1 |val f: [T] => (x: T) => x.type = [T] => (x: Int) => x // error | ^^^^^^^^^^^^^^^^^^^^ | Found: [T] => (x: Int) => x.type - | Required: [T] => (x: T) => x.type + | Required: [T] => (x²: T) => x².type + | + | where: x is a reference to a value parameter + | x² is a reference to a value parameter | | longer explanation available when compiling with `-explain` From 8e67d6cdfff283a257b382078a0b65d24870e35d Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Feb 2025 11:21:38 +0100 Subject: [PATCH 62/93] Refactoring: Separate addOwnerAsHidden from Fresh creation Also, work towards linking Fresh.fromCap more tightly to transformExplicitType --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 6 ++-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../src/dotty/tools/dotc/cc/Existential.scala | 4 +-- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 22 +++++--------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 29 +++++++++++++++---- .../neg-custom-args/captures/sepchecks2.check | 6 ++-- 7 files changed, 42 insertions(+), 29 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f99f0c165ff8..8e7e9cbbda1a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -764,7 +764,7 @@ object CapsOfApply: abstract class AnnotatedCapability(annotCls: Context ?=> ClassSymbol): def apply(tp: Type)(using Context): AnnotatedType = - assert(tp.isTrackableRef) + assert(tp.isTrackableRef, i"not a trackable ref: $tp") tp match case AnnotatedType(_, annot) => assert(!unwrappable.contains(annot.symbol), i"illegal combination of derived capabilities: $annotCls over ${annot.symbol}") diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 366d1d2f426d..4acaeeb4c018 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -419,7 +419,7 @@ object CaptureSet: defn.captureRoot.termRef.singletonCaptureSet def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = - Fresh(owner).singletonCaptureSet + Fresh.withOwner(owner).singletonCaptureSet /** The shared capture set `{cap.rd}` */ def shared(using Context): CaptureSet = @@ -952,8 +952,8 @@ object CaptureSet: * which are already subject through snapshotting and rollbacks in VarState. * It's advantageous if we don't need to deal with other pieces of state there. */ - class HiddenSet(initialOwner: Symbol, initialHidden: Refs = emptyRefs)(using @constructorOnly ictx: Context) - extends Var(initialOwner, initialHidden): + class HiddenSet(initialOwner: Symbol)(using @constructorOnly ictx: Context) + extends Var(initialOwner): var owningCap: AnnotatedType = uninitialized var givenOwner: Symbol = initialOwner diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 69cc650dbed5..245699d9d5be 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -846,7 +846,7 @@ class CheckCaptures extends Recheck, SymTransformer: var refined: Type = core var allCaptures: CaptureSet = if core.derivesFromMutable then initCs ++ CaptureSet.fresh() - else if core.derivesFromCapability then initCs ++ Fresh(core.classSymbol).readOnly.singletonCaptureSet + else if core.derivesFromCapability then initCs ++ Fresh.withOwner(core.classSymbol).readOnly.singletonCaptureSet else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 1aaf1a8a249e..a405cb6401b0 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -234,7 +234,7 @@ object Existential: def apply(t: Type): Type = t match case t @ Vble(binder) => if localBinders.contains(binder) then t // keep bound references - else seen.getOrElseUpdate(t.annot, Fresh(NoSymbol)) // map free references to Fresh() + else seen.getOrElseUpdate(t.annot, Fresh()) // map free references to Fresh() case t: MethodType => // skip parameters val saved = localBinders @@ -306,7 +306,7 @@ object Existential: val (k, v) = it.next if v.annot eq t.annot then ref = k if ref == null then - ref = Fresh(NoSymbol) + ref = Fresh() seen(ref) = t ref case _ => mapOver(t) diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 6d3209302e78..5a88673ae4af 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -47,9 +47,9 @@ object Fresh: end Annot /** Constructor and extractor methods for "fresh" capabilities */ - def apply(owner: Symbol, initialHidden: Refs = emptyRefs)(using Context): CaptureRef = + private def make(owner: Symbol)(using Context): CaptureRef = if ccConfig.useSepChecks then - val hiddenSet = CaptureSet.HiddenSet(owner, initialHidden) + val hiddenSet = CaptureSet.HiddenSet(owner) val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet, NoType)) hiddenSet.owningCap = res //assert(hiddenSet.id != 3) @@ -57,11 +57,9 @@ object Fresh: else defn.captureRoot.termRef - def apply(owner: Symbol, reach: Boolean)(using Context): CaptureRef = - apply(owner, ownerToHidden(owner, reach)) + def withOwner(owner: Symbol)(using Context): CaptureRef = make(owner) - def apply(owner: Symbol)(using Context): CaptureRef = - apply(owner, ownerToHidden(owner, reach = false)) + def apply()(using Context): CaptureRef = make(NoSymbol) def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match case Annot(hidden, binder) if !binder.exists => Some(hidden) @@ -69,7 +67,7 @@ object Fresh: /** Create an existential */ def existential(binder: MethodType)(using Context): AnnotatedType = - val hiddenSet = CaptureSet.HiddenSet(NoSymbol, emptyRefs) + val hiddenSet = CaptureSet.HiddenSet(NoSymbol) val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet, binder)) hiddenSet.owningCap = res res @@ -88,17 +86,13 @@ object Fresh: class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: thisMap => - private var reach = false - override def apply(t: Type) = if variance <= 0 then t else t match case t: CaptureRef if t.isCap => - Fresh(owner, ownerToHidden(owner, reach)) - case t @ CapturingType(_, refs) => - val savedReach = reach - if t.isBoxed then reach = true - try mapOver(t) finally reach = savedReach + Fresh.withOwner(owner) + case t @ CapturingType(_, _) => + mapOver(t) case t @ AnnotatedType(parent, ann) => val parent1 = this(parent) if ann.symbol.isRetains && ann.tree.toCaptureSet.containsCap then diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 1402d566af7f..4ea70739f5cc 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -14,6 +14,7 @@ import transform.{PreRecheck, Recheck}, Recheck.* import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded import util.SimpleIdentitySet +import util.chaining.* import reporting.Message import printing.{Printer, Texts}, Texts.{Text, Str} import collection.mutable @@ -132,7 +133,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def mappedInfo = if toBeUpdated.contains(sym) then symd.info // don't transform symbols that will anyway be updated - else Fresh.fromCap(transformExplicitType(symd.info, sym), sym) + else Fresh.fromCap(transformExplicitType(symd.info, sym), sym).tap(addOwnerAsHidden(_, sym)) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) else if isPreCC(sym) then @@ -489,6 +490,22 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: extension (sym: Symbol) def nextInfo(using Context): Type = atPhase(thisPhase.next)(sym.info) + private def addOwnerAsHidden(tp: Type, owner: Symbol)(using Context): Unit = + val ref = owner.termRef + def add = new TypeTraverser: + var reach = false + def traverse(t: Type): Unit = t match + case Fresh(hidden) => + if reach then hidden.elems += ref.reach + else if ref.isTracked then hidden.elems += ref + case t @ CapturingType(_, _) if t.isBoxed && !reach => + reach = true + try traverseChildren(t) finally reach = false + case _ => + traverseChildren(t) + if ref.isTrackableRef then add.traverse(tp) + end addOwnerAsHidden + /** A traverser that adds knownTypes and updates symbol infos */ def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts: import checker.* @@ -503,15 +520,17 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: var transformed = if tree.isInferred then transformInferredType(tree.tpe) - else transformExplicitType(tree.tpe, sym, tptToCheck = tree) + else + val transformed = transformExplicitType(tree.tpe, sym, tptToCheck = tree) + if boxed then transformed + else Fresh.fromCap(transformed, sym).tap(addOwnerAsHidden(_, sym)) if boxed then transformed = box(transformed) if sym.is(Param) && (transformed ne tree.tpe) then paramSigChange += tree tree.setNuType( if boxed then transformed else if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) - else if tree.isInferred then transformed - else Fresh.fromCap(transformed, sym)) + else transformed) /** Transform the type of a val or var or the result type of a def */ def transformResultType(tpt: TypeTree, sym: Symbol)(using Context): Unit = @@ -899,7 +918,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def apply(t: Type) = t match case t @ CapturingType(parent, refs) => val parent1 = this(parent) - if refs.isUniversal then t.derivedCapturingType(parent1, CaptureSet.Fluid) + if refs.isUniversalOrFresh then t.derivedCapturingType(parent1, CaptureSet.Fluid) else t case _ => mapFollowingAliases(t) diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check index 3dd0306bfe0e..47a80306f3b7 100644 --- a/tests/neg-custom-args/captures/sepchecks2.check +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -10,11 +10,11 @@ | Separation failure: argument of type List[box () ->{c} Unit] | to method foo: (xs: List[box () => Unit], y: Object^): Nothing | corresponds to capture-polymorphic formal parameter xs of type List[box () => Unit] - | and hides capabilities {*, c}. + | and hides capabilities {c}. | Some of these overlap with the captures of the second argument with type (c : Object^). | - | Hidden set of current argument : {*, c} - | Hidden footprint of current argument : {*, c} + | Hidden set of current argument : {c} + | Hidden footprint of current argument : {c} | Capture set of second argument : {c} | Footprint set of second argument : {c} | The two sets overlap at : {c} From aa06e074381bd8d408d58f2997fd657b1fff5786 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 1 Mar 2025 13:26:03 +0100 Subject: [PATCH 63/93] Expand cap arguments of function aliases before de-aliasing This is needed to that we can express functions where the result existential scopes over a method type before the directly enclosing one. Example, from `cc-existential-conformance.scala`: type Fun[X] = A -> X A -> A -> B^ prints as A -> A -> B^ A -> Fun[B^] prints as A -> A -> B^{outer_cap} That is, the B^ is tied to the outer function. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 32 ++++++---- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 2 + .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 21 ++++--- .../src/dotty/tools/dotc/cc/Existential.scala | 8 +-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 61 ++++++++++++------- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../tools/dotc/printing/PlainPrinter.scala | 8 +-- .../tools/dotc/printing/RefinedPrinter.scala | 14 +++-- .../captures/cc-existential-conformance.check | 7 +++ .../captures/cc-existential-conformance.scala | 14 +++++ 11 files changed, 111 insertions(+), 60 deletions(-) create mode 100644 tests/neg-custom-args/captures/cc-existential-conformance.check create mode 100644 tests/neg-custom-args/captures/cc-existential-conformance.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 8e7e9cbbda1a..d54cc41ac881 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -101,7 +101,7 @@ class CCState: private var curLevel: Level = outermostLevel private val symLevel: mutable.Map[Symbol, Int] = mutable.Map() - private var openedFreshBinders: List[MethodType] = Nil + private var openExistentialScopes: List[MethodType] = Nil object CCState: @@ -134,20 +134,21 @@ object CCState: if !p then ccs.curLevel = ccs.curLevel.nextInner try op finally ccs.curLevel = saved - /** If we are currently in capture checking or setup, perform `op` assuming - * a fresh enclosing binder `mt`, otherwise perform `op` directly. + /** If we are currently in capture checking or setup, and `mt` is a method + * type that is not a prefix of a curried method, perform `op` assuming + * a fresh enclosing existential scope `mt`, otherwise perform `op` directly. */ - inline def inOpenedFreshBinder[T](mt: MethodType)(op: => T)(using Context): T = + inline def inNewExistentialScope[T](mt: MethodType)(op: => T)(using Context): T = if isCaptureCheckingOrSetup then val ccs = ccState - val saved = ccs.openedFreshBinders - if mt.isFreshBinder then ccs.openedFreshBinders = mt :: ccs.openedFreshBinders - try op finally ccs.openedFreshBinders = saved + val saved = ccs.openExistentialScopes + if mt.marksExistentialScope then ccs.openExistentialScopes = mt :: ccs.openExistentialScopes + try op finally ccs.openExistentialScopes = saved else op - /** The currently opened fresh binders */ - def openedFreshBinders(using Context): List[MethodType] = ccState.openedFreshBinders + /** The currently opened existential scopes */ + def openExistentialScopes(using Context): List[MethodType] = ccState.openExistentialScopes extension (x: Level) def isDefined: Boolean = x >= 0 @@ -590,14 +591,19 @@ extension (tp: Type) case tp: ThisType => tp.cls.ccLevel.nextInner case _ => undefinedLevel + /** Is this a method or function that has `other` as its direct or indirect result + * type? + */ def hasSuffix(other: MethodType)(using Context): Boolean = (tp eq other) || tp.match - case tp: MethodOrPoly => tp.resType.hasSuffix(other) + case defn.RefinedFunctionOf(mt) => mt.hasSuffix(other) + case mt: MethodType => mt.resType.hasSuffix(other) case _ => false - def isFreshBinder(using Context): Boolean = tp match - case tp: MethodType => !tp.resType.isInstanceOf[MethodOrPoly] - case _ => false +extension (tp: MethodType) + /** A method marks an existential scope unless it is the prefix of a curried method */ + def marksExistentialScope(using Context): Boolean = + !tp.resType.isInstanceOf[MethodOrPoly] extension (cls: ClassSymbol) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 786afa129066..1e52a9be4f3c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -14,6 +14,7 @@ import compiletime.uninitialized import StdNames.nme import CaptureSet.VarState import Annotations.Annotation +import config.Printers.capt /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, * as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities. @@ -259,6 +260,7 @@ trait CaptureRef extends TypeProxy, ValueType: case Existential.Vble(binder) => y.stripReadOnly match case Existential.Vble(binder1) => binder1.hasSuffix(binder) + .showing(i"cmp existential $binder maxSubsumes $binder1 = $result", capt) case _ => true case _ => this.isCap && canAddHidden && vs != VarState.HardSeparate diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 4acaeeb4c018..bc4366192faf 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -572,7 +572,7 @@ object CaptureSet: else elem match case elem @ Existential.Vble(mt) => !noUniversal - && !CCState.openedFreshBinders.contains(elem) + && !CCState.openExistentialScopes.contains(elem) // Opened existentials on the left cannot be added to nested capture sets on the right // of a comparison. Test case is open-existential.scala. case elem: TermRef if level.isDefined => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 245699d9d5be..860452a0a577 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -165,7 +165,10 @@ object CheckCaptures: val check = new TypeTraverser: private val seen = new EqHashSet[TypeRef] - var openFreshBinders: List[MethodType] = Nil + + // We keep track of open existential scopes here so that we can set these scopes + // in ccState when printing a part of the offending type. + var openExistentialScopes: List[MethodType] = Nil def traverse(t: Type) = t.dealiasKeepAnnots match @@ -184,18 +187,18 @@ object CheckCaptures: () case CapturingType(parent, refs) => if variance >= 0 then - val openBinders = openFreshBinders + val openScopes = openExistentialScopes refs.disallowRootCapability: () => def part = if t eq tp then "" else - // Show in context of all enclosing traversed fresh binders. + // Show in context of all enclosing traversed existential scopes. def showInOpenedFreshBinders(mts: List[MethodType]): String = mts match case Nil => i"the part $t of " case mt :: mts1 => - CCState.inOpenedFreshBinder(mt): + CCState.inNewExistentialScope(mt): showInOpenedFreshBinders(mts1) - showInOpenedFreshBinders(openBinders.reverse) + showInOpenedFreshBinders(openScopes.reverse) report.error( em"""$what cannot $have $tp since |${part}that type captures the root capability `cap`.$addendum""", @@ -203,13 +206,13 @@ object CheckCaptures: traverse(parent) case defn.RefinedFunctionOf(mt) => traverse(mt) - case t: MethodType if t.isFreshBinder => + case t: MethodType if t.marksExistentialScope => atVariance(-variance): t.paramInfos.foreach(traverse) - val saved = openFreshBinders - openFreshBinders = t :: openFreshBinders + val saved = openExistentialScopes + openExistentialScopes = t :: openExistentialScopes try traverse(t.resType) - finally openFreshBinders = saved + finally openExistentialScopes = saved case t => traverseChildren(t) if ccConfig.useSealed then check.traverse(tp) diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index a405cb6401b0..253dd3791daf 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -238,7 +238,7 @@ object Existential: case t: MethodType => // skip parameters val saved = localBinders - if t.isFreshBinder then localBinders = localBinders + t + if t.marksExistentialScope then localBinders = localBinders + t try t.derivedLambdaType(resType = this(t.resType)) finally localBinders = saved case t: PolyType => @@ -318,13 +318,13 @@ object Existential: end mapCap /** Map `cap` in function results to fresh existentials */ - def mapCapInResults(fail: Message => Unit)(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def mapCapInResults(fail: Message => Unit, keepAliases: Boolean = false)(using Context): TypeMap = new TypeMap with FollowAliasesMap: def apply(t: Type): Type = t match case defn.RefinedFunctionOf(mt) => val mt1 = apply(mt) if mt1 ne mt then mt1.toFunctionType(alwaysDependent = true) else t - case t: MethodType if variance > 0 && t.isFreshBinder => + case t: MethodType if variance > 0 && t.marksExistentialScope => val t1 = mapOver(t).asInstanceOf[MethodType] t1.derivedLambdaType(resType = mapCap(t1.resType, t1, fail)) case CapturingType(parent, refs) => @@ -332,7 +332,7 @@ object Existential: case t: (LazyRef | TypeVar) => mapConserveSuper(t) case _ => - mapFollowingAliases(t) + if keepAliases then mapOver(t) else mapFollowingAliases(t) end mapCapInResults end Existential diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 4ea70739f5cc..4c1b5e13b34c 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -201,17 +201,19 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Map parametric functions with results that have a capture set somewhere * to dependent functions. */ - protected def normalizeFunctions(tp: Type, original: Type)(using Context): Type = tp match + protected def normalizeFunctions(tp: Type, original: Type, expandAlways: Boolean = false)(using Context): Type = + tp match case AppliedType(tycon, args) if defn.isNonRefinedFunction(tp) && isTopLevel => - original match - case AppliedType(`tycon`, args0) if args0.last ne args.last => - // We have an applied type that underwent some addition of capture sets. - // Map to a dependent type so that things are more uniform. - depFun(args.init, args.last, - isContextual = defn.isContextFunctionClass(tycon.classSymbol)) - .showing(i"add function refinement $tp ($tycon, ${args.init}, ${args.last}) --> $result", capt) - case _ => tp + // Expand if we have an applied type that underwent some addition of capture sets + val expand = expandAlways || original.match + case AppliedType(`tycon`, args0) => args0.last ne args.last + case _ => false + if expand then + depFun(args.init, args.last, + isContextual = defn.isContextFunctionClass(tycon.classSymbol)) + .showing(i"add function refinement $tp ($tycon, ${args.init}, ${args.last}) --> $result", capt) + else tp case _ => tp /** Pull out an embedded capture set from a part of `tp` */ @@ -334,8 +336,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def fail(msg: Message) = if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) - val toCapturing = new DeepTypeMap with SetupTypeMap: - override def toString = "expand aliases" + object toCapturing extends DeepTypeMap, SetupTypeMap: + override def toString = "transformExplicitType" + + var keepFunAliases = true + var keptFunAliases = false /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib * are defined with `?=>` rather than `?->`. @@ -425,21 +430,33 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case throwsAlias(res, exc) => this(expandThrowsAlias(res, exc, Nil)) case t @ AppliedType(tycon, args) - if defn.isNonRefinedFunction(tp) - && !defn.isFunctionSymbol(tp.typeSymbol) && (tp.dealias ne tp) => - // Expand arguments of aliases of function types before proceeding with dealias. - // This is necessary to bind existentialFresh instances to the right method binder. - val args1 = atVariance(-variance): - args.map(this) - defaultApply(t.derivedAppliedType(tycon, args1)) + if defn.isNonRefinedFunction(t) + && !defn.isFunctionSymbol(t.typeSymbol) && (t.dealias ne tp) => + if keepFunAliases then + // Hold off with dealising and expand in a second pass. + // This is necessary to bind existentialFresh instances to the right method binder. + keptFunAliases = true + mapOver(t) + else + // In the second pass, map the alias and make sure it has the form + // of a dependent function. + normalizeFunctions(apply(t.dealias), t, expandAlways = true) case t => defaultApply(t) end toCapturing - val tp1 = toCapturing(tp) - val tp2 = Existential.mapCapInResults(fail)(tp1) - if tp2 ne tp then capt.println(i"expanded explicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") - tp2 + def transform(tp: Type): Type = + val tp1 = toCapturing(tp) + val tp2 = Existential.mapCapInResults(fail, toCapturing.keepFunAliases)(tp1) + val snd = if toCapturing.keepFunAliases then "" else " 2nd time" + if tp2 ne tp then capt.println(i"expanded explicit$snd in ${ctx.owner}: $tp --> $tp1 --> $tp2") + tp2 + + val tp1 = transform(tp) + if toCapturing.keptFunAliases then + toCapturing.keepFunAliases = false + transform(tp1) + else tp1 end transformExplicitType /** Substitute parameter symbols in `from` to paramRefs in corresponding diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 3e9ad3f55a2c..f4f9ac6b97a4 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -804,7 +804,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling (tp1.signature consistentParams tp2.signature) && matchingMethodParams(tp1, tp2) && (!tp2.isImplicitMethod || tp1.isImplicitMethod) && - CCState.inOpenedFreshBinder(tp2): + CCState.inNewExistentialScope(tp2): isSubType(tp1.resultType, tp2.resultType.subst(tp2, tp1)) case _ => false } diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index d6cf8907ed23..f5bdbc19af69 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -260,9 +260,9 @@ class PlainPrinter(_ctx: Context) extends Printer { def isUniversal = refs.elems.size == 1 && (refs.isUniversal - || refs.elems.nth(0).match + || !printDebug && !showUniqueIds && refs.elems.nth(0).match case Existential.Vble(binder) => - CCState.openedFreshBinders match + CCState.openExistentialScopes match case b :: _ => binder eq b case _ => false case _ => @@ -301,7 +301,7 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ paramsText(tp) ~ ")" ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) - ~ CCState.inOpenedFreshBinder(tp)(toText(tp.resultType)) + ~ CCState.inNewExistentialScope(tp)(toText(tp.resultType)) } case ExprType(restp) => def arrowText: Text = restp match @@ -456,7 +456,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" case Existential.Vble(binder) => // TODO: Better printing? USe a mode where we print more detailed - val vbleText: Text = CCState.openedFreshBinders.indexOf(binder) match + val vbleText: Text = CCState.openExistentialScopes.indexOf(binder) match case -1 => "" case n => "outer_" * n ++ "cap" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 32cbd4b07887..fef0bee16465 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -165,9 +165,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { val capturesRoot = refs == rootSetText val isPure = Feature.pureFunsEnabled && !tsym.name.isImpureFunction && !capturesRoot - toTextFunction(args.init, args.last, refs.provided(!capturesRoot), isContextual, isPure) + toTextFunction(args.init, args.last, tp, refs.provided(!capturesRoot), isContextual, isPure) - private def toTextFunction(args: List[Type], res: Type, refs: Text, + private def toTextFunction(args: List[Type], res: Type, fn: MethodType | AppliedType, refs: Text, isContextual: Boolean, isPure: Boolean): Text = changePrec(GlobalPrec): val argStr: Text = args match @@ -178,7 +178,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "(" ~ argsText(args) ~ ")" - argStr ~ " " ~ arrow(isContextual, isPure) ~ refs ~ " " ~ argText(res) + argStr ~ " " ~ arrow(isContextual, isPure) ~ refs ~ " " + ~ fn.match + case fn: MethodType => CCState.inNewExistentialScope(fn)(argText(res)) + case _ => argText(res) protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = def recur(tp: Type, enclInfo: MethodType | Null): Text = tp match @@ -190,8 +193,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { && !showUniqueIds && !printDebug then // cc.Setup converts all functions to dependent functions. Undo that when printing. - CCState.inOpenedFreshBinder(tp): - toTextFunction(tp.paramInfos, tp.resType, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) + toTextFunction(tp.paramInfos, tp.resType, tp, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) else changePrec(GlobalPrec): "(" @@ -209,7 +211,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { ~ recur(tp.resultType, enclInfo) } case _ => - if enclInfo != null then CCState.inOpenedFreshBinder(enclInfo)(toText(tp)) + if enclInfo != null then CCState.inNewExistentialScope(enclInfo)(toText(tp)) else toText(tp) recur(info, null) diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.check b/tests/neg-custom-args/captures/cc-existential-conformance.check new file mode 100644 index 000000000000..7fa04c93fe3f --- /dev/null +++ b/tests/neg-custom-args/captures/cc-existential-conformance.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:8:24 -------------------- +8 | val y: A -> Fun[B^] = x // error + | ^ + | Found: (x : A -> A -> B^) + | Required: A -> A -> B^{outer_cap} + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.scala b/tests/neg-custom-args/captures/cc-existential-conformance.scala new file mode 100644 index 000000000000..9c602aebac9a --- /dev/null +++ b/tests/neg-custom-args/captures/cc-existential-conformance.scala @@ -0,0 +1,14 @@ +class A +class B + +type Fun[T] = A -> T + +def test() = + val x: A -> A -> B^ = ??? + val y: A -> Fun[B^] = x // error + val z: A -> A -> B^ = y // ok + +def test2() = + val x: A -> B^ = ??? + val y: Fun[B^] = x // should be error + val z: A -> B^ = y // ok From 4e6b82ee6ef1743ef5b8c8caba297dee9de0120f Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 1 Mar 2025 13:50:18 +0100 Subject: [PATCH 64/93] Always use the sealed policy Drop the exception that held only in 3.5 --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 9 --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 75 ++----------------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 9 +-- tests/neg-custom-args/captures/unbox.scala | 6 -- tests/pos-custom-args/captures/hk-param.scala | 18 ----- tests/pos-custom-args/captures/levels.scala | 25 ------- 6 files changed, 7 insertions(+), 135 deletions(-) delete mode 100644 tests/neg-custom-args/captures/unbox.scala delete mode 100644 tests/pos-custom-args/captures/hk-param.scala delete mode 100644 tests/pos-custom-args/captures/levels.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index d54cc41ac881..a8731a20081b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -43,15 +43,6 @@ object ccConfig: */ inline val deferredReaches = false - /** If true, use "sealed" as encapsulation mechanism, meaning that we - * check that type variable instantiations don't have `cap` in any of - * their capture sets. This is an alternative of the original restriction - * that `cap` can't be boxed or unboxed. It is dropped in 3.5 but used - * again in 3.6. - */ - def useSealed(using Context) = - Feature.sourceVersion.stable != SourceVersion.`3.5` - /** If true, turn on separation checking */ def useSepChecks(using Context): Boolean = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 860452a0a577..f0b73998d368 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -215,42 +215,9 @@ object CheckCaptures: finally openExistentialScopes = saved case t => traverseChildren(t) - if ccConfig.useSealed then check.traverse(tp) + check.traverse(tp) end disallowRootCapabilitiesIn - /** If we are not under the sealed policy, and a tree is an application that unboxes - * its result or is a try, check that the tree's type does not have covariant universal - * capabilities. - */ - private def checkNotUniversalInUnboxedResult(tpe: Type, tree: Tree)(using Context): Unit = - def needsUniversalCheck = tree match - case _: RefTree | _: Apply | _: TypeApply => tree.symbol.unboxesResult - case _: Try => true - case _ => false - - object checkNotUniversal extends TypeTraverser: - def traverse(tp: Type) = - tp.dealias match - case wtp @ CapturingType(parent, refs) => - if variance > 0 then - refs.disallowRootCapability: () => - def part = if wtp eq tpe.widen then "" else i" in its part $wtp" - report.error( - em"""The expression's type ${tpe.widen} is not allowed to capture the root capability `cap`$part. - |This usually means that a capability persists longer than its allowed lifetime.""", - tree.srcPos) - if !wtp.isBoxed then traverse(parent) - case tp => - traverseChildren(tp) - - if !ccConfig.useSealed - && !tpe.hasAnnotation(defn.UncheckedCapturesAnnot) - && needsUniversalCheck - && tpe.widen.isValueType - then - checkNotUniversal.traverse(tpe.widen) - end checkNotUniversalInUnboxedResult - trait CheckerAPI: /** Complete symbol info of a val or a def */ def completeDef(tree: ValOrDefDef, sym: Symbol)(using Context): Type @@ -584,7 +551,7 @@ class CheckCaptures extends Recheck, SymTransformer: */ def disallowCapInTypeArgs(fn: Tree, sym: Symbol, args: List[Tree])(using Context): Unit = def isExempt = sym.isTypeTestOrCast || sym == defn.Compiletime_erasedValue - if ccConfig.useSealed && !isExempt then + if !isExempt then val paramNames = atPhase(thisPhase.prev): fn.tpe.widenDealias match case tl: TypeLambda => tl.paramNames @@ -1178,7 +1145,7 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def recheckTry(tree: Try, pt: Type)(using Context): Type = val tp = super.recheckTry(tree, pt) - if ccConfig.useSealed && Feature.enabled(Feature.saferExceptions) then + if Feature.enabled(Feature.saferExceptions) then disallowRootCapabilitiesIn(tp, ctx.owner, "The result of `try`", "have type", "\nThis is often caused by a locally generated exception capability leaking as part of its result.", @@ -1227,12 +1194,6 @@ class CheckCaptures extends Recheck, SymTransformer: 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 = - checkNotUniversalInUnboxedResult(tpe, tree) - super.recheckFinish(tpe, tree, pt) - end recheckFinish - // ------------------ Adaptation ------------------------------------- // // Adaptations before checking conformance of actual vs expected: @@ -1487,34 +1448,12 @@ class CheckCaptures extends Recheck, SymTransformer: .capturing(if alwaysConst then CaptureSet(captures.elems) else captures) .forceBoxStatus(resultBoxed) - if needsAdaptation then - val criticalSet = // the set with which we box or unbox + if needsAdaptation && !insertBox then // we are unboxing + val criticalSet = // the set with which we unbox if covariant then captures // covariant: we box with captures of actual type plus captures leaked by inner adapation else expected.captureSet // contravarant: we unbox with captures of epected type - def msg = em"""$actual cannot be box-converted to $expected - |since at least one of their capture sets contains the root capability `cap`""" - def allowUniversalInBoxed = - ccConfig.useSealed - || expected.hasAnnotation(defn.UncheckedCapturesAnnot) - || actual.widen.hasAnnotation(defn.UncheckedCapturesAnnot) - if !allowUniversalInBoxed then - if criticalSet.isUnboxable && expected.isValueType then - // We can't box/unbox the universal capability. Leave `actual` as it is - // so we get an error in checkConforms. Add the error message generated - // from boxing as an addendum. This tends to give better error - // messages than disallowing the root capability in `criticalSet`. - if boxErrors != null then boxErrors += msg - if ctx.settings.YccDebug.value then - println(i"cannot box/unbox $actual vs $expected") - return actual - // Disallow future addition of `cap` to `criticalSet`. - criticalSet.disallowRootCapability: () => - report.error(msg, tree.srcPos) - - if !insertBox then // we are unboxing //debugShowEnvs() - markFree(criticalSet, tree) - end if + markFree(criticalSet, tree) // Compute the adapted type. // The result is boxed if actual is boxed and we don't need to adapt, @@ -1901,8 +1840,6 @@ class CheckCaptures extends Recheck, SymTransformer: checkBounds(normArgs, tl) args.lazyZip(tl.paramNames).foreach(healTypeParam(_, _, fun.symbol)) case _ => - case tree: TypeTree if !ccConfig.useSealed => - checkArraysAreSealedIn(tree.tpe, tree.srcPos) case _ => end check end checker diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 4c1b5e13b34c..08252a55883d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -553,14 +553,7 @@ 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, 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. - ) + transformTT(tpt, sym, boxed = false) catch case ex: IllegalCaptureRef => capt.println(i"fail while transforming result type $tpt of $sym") throw ex diff --git a/tests/neg-custom-args/captures/unbox.scala b/tests/neg-custom-args/captures/unbox.scala deleted file mode 100644 index 28feb5f89aff..000000000000 --- a/tests/neg-custom-args/captures/unbox.scala +++ /dev/null @@ -1,6 +0,0 @@ -import language.`3.5` -type Proc = () => Unit - -val xs: List[Proc] = ??? - -val x = xs.head // error diff --git a/tests/pos-custom-args/captures/hk-param.scala b/tests/pos-custom-args/captures/hk-param.scala deleted file mode 100644 index 325a2b55a480..000000000000 --- a/tests/pos-custom-args/captures/hk-param.scala +++ /dev/null @@ -1,18 +0,0 @@ -//> using options -source 3.5 -// (to make sure we use the unsealed policy) -/** Concrete collection type: View */ -trait View[+A] extends Itable[A], ILike[A, [X] =>> View[X]^]: - override def fromIterable[B](c: Itable[B]^): View[B]^{c} = ??? - -trait IPolyTransforms[+A, +C[A]] extends Any: - def fromIterable[B](coll: Itable[B]^): C[B] - -trait ILike[+A, +C[X] <: Itable[X]^] extends IPolyTransforms[A, C] - -/** Base trait for generic collections */ -trait Itable[+A] extends ItableOnce[A] with ILike[A, Itable^] - -/** Iterator can be used only once */ -trait ItableOnce[+A] { - def iterator: Iterator[A]^{this} -} diff --git a/tests/pos-custom-args/captures/levels.scala b/tests/pos-custom-args/captures/levels.scala deleted file mode 100644 index 4d9d759e86db..000000000000 --- a/tests/pos-custom-args/captures/levels.scala +++ /dev/null @@ -1,25 +0,0 @@ -//> using options -source 3.5 -// (to make sure we use the unsealed policy) -class CC - -def test1(cap1: CC^) = - - class Ref[T](init: T): - private var v: T = init - def setV(x: T): Unit = v = x - def getV: T = v - -def test2(cap1: CC^) = - - class Ref[T](init: T): - private var v: T = init - def setV(x: T): Unit = v = x - def getV: T = v - - val _ = Ref[String => String]((x: String) => x) - val r = Ref((x: String) => x) - - def scope(cap3: CC^) = - def g(x: String): String = if cap3 == cap3 then "" else "a" - r.setV(g) - () From 0f0a5b3bc500c529038b0a222facdec8b007c51a Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 1 Mar 2025 14:00:04 +0100 Subject: [PATCH 65/93] Refactoring: Move Fresh.fromCap into transformExplicitType --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 25 ++++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 08252a55883d..b4ad91343efb 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -133,7 +133,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def mappedInfo = if toBeUpdated.contains(sym) then symd.info // don't transform symbols that will anyway be updated - else Fresh.fromCap(transformExplicitType(symd.info, sym), sym).tap(addOwnerAsHidden(_, sym)) + else transformExplicitType(symd.info, sym, freshen = true) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) else if isPreCC(sym) then @@ -331,7 +331,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 5. Schedule deferred well-formed tests for types with retains annotations. * 6. Perform normalizeCaptures */ - private def transformExplicitType(tp: Type, sym: Symbol, tptToCheck: Tree = EmptyTree)(using Context): Type = + private def transformExplicitType(tp: Type, sym: Symbol, freshen: Boolean, tptToCheck: Tree = EmptyTree)(using Context): Type = def fail(msg: Message) = if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) @@ -453,10 +453,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tp2 val tp1 = transform(tp) - if toCapturing.keptFunAliases then - toCapturing.keepFunAliases = false - transform(tp1) - else tp1 + val tp2 = + if toCapturing.keptFunAliases then + toCapturing.keepFunAliases = false + transform(tp1) + else tp1 + if freshen then Fresh.fromCap(tp2).tap(addOwnerAsHidden(_, sym)) + else tp2 end transformExplicitType /** Substitute parameter symbols in `from` to paramRefs in corresponding @@ -537,16 +540,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: var transformed = if tree.isInferred then transformInferredType(tree.tpe) - else - val transformed = transformExplicitType(tree.tpe, sym, tptToCheck = tree) - if boxed then transformed - else Fresh.fromCap(transformed, sym).tap(addOwnerAsHidden(_, sym)) + else transformExplicitType(tree.tpe, sym, freshen = !boxed, tptToCheck = tree) if boxed then transformed = box(transformed) if sym.is(Param) && (transformed ne tree.tpe) then paramSigChange += tree tree.setNuType( - if boxed then transformed - else if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) + if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) else transformed) /** Transform the type of a val or var or the result type of a def */ @@ -758,7 +757,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // Compute new parent types val ps1 = inContext(ctx.withOwner(cls)): - ps.mapConserve(transformExplicitType(_, NoSymbol)) + ps.mapConserve(transformExplicitType(_, NoSymbol, freshen = false)) // Install new types and if it is a module class also update module object if (selfInfo1 ne selfInfo) || (ps1 ne ps) then From 30971b574332dfe735d2c6886e30dee7f6b74633 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 3 Mar 2025 20:06:12 +0100 Subject: [PATCH 66/93] Open existential scopes only for `(x: T) => U` functions Parametric functions `A -> B` or `A => B` do not open an existential scope anymore. Hence, a function of the form ```scala def f(x: A => B^) ``` is capture polymorphic in both the function effect and the function result effect. On the other hand, ```scala def f(x: (y: A) => B^) ``` would be a function that can be passed only arguments that return a fresh result on each call. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 12 +++--- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 9 +++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../src/dotty/tools/dotc/cc/Existential.scala | 3 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 7 ++-- .../dotty/tools/dotc/core/TypeComparer.scala | 8 ++-- .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../tools/dotc/printing/PlainPrinter.scala | 4 +- .../tools/dotc/printing/RefinedPrinter.scala | 4 +- .../captures/boundschecks3.check | 28 +++++++++++-- .../captures/boundschecks3.scala | 8 ++-- .../captures/cc-existential-conformance.check | 17 +++++++- .../captures/cc-existential-conformance.scala | 6 +-- .../captures/depfun-reach.check | 6 +-- .../captures/existential-mapping.check | 41 ++++++++++--------- .../captures/existential-mapping.scala | 3 +- tests/neg-custom-args/captures/i21614.check | 2 +- .../captures/leaked-curried.check | 8 ++++ .../captures/leaked-curried.scala | 5 ++- tests/neg-custom-args/captures/reaches.check | 35 +++++++++++----- tests/neg-custom-args/captures/reaches.scala | 10 ++++- tests/neg-custom-args/captures/sep-use2.check | 18 +++++++- tests/neg-custom-args/captures/sep-use2.scala | 12 ++++-- .../captures/list-encoding.scala | 2 +- tests/pos-custom-args/captures/skolems2.scala | 4 +- 25 files changed, 175 insertions(+), 81 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a8731a20081b..cd3645f0b1f6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -540,11 +540,13 @@ extension (tp: Type) case t @ AnnotatedType(parent, ann) => // Don't map annotations, which includes capture sets t.derivedAnnotatedType(this(parent), ann) - case t @ FunctionOrMethod(args, res) - if args.forall(_.isAlwaysPure) => - // Also map existentials in results to reach capabilities if all - // preceding arguments are known to be always pure - t.derivedFunctionOrMethod(args, apply(Existential.toCap(res))) + case t @ FunctionOrMethod(args, res) => + if args.forall(_.isAlwaysPure) then + // Also map existentials in results to reach capabilities if all + // preceding arguments are known to be always pure + t.derivedFunctionOrMethod(args, apply(Existential.toCap(res))) + else + t case _ => mapOver(t) end narrowCaps diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 1e52a9be4f3c..3fa524933f67 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -252,18 +252,23 @@ trait CaptureRef extends TypeProxy, ValueType: * fail a comparison. */ def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = + def yIsExistential = y.stripReadOnly match + case Existential.Vble(_) => + capt.println(i"failed existential $this >: $y") + true + case _ => false (this eq y) || this.match case Fresh(hidden) => vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) - || !y.stripReadOnly.isCap && canAddHidden && vs.addHidden(hidden, y) + || !y.stripReadOnly.isCap && !yIsExistential && canAddHidden && vs.addHidden(hidden, y) case Existential.Vble(binder) => y.stripReadOnly match case Existential.Vble(binder1) => binder1.hasSuffix(binder) .showing(i"cmp existential $binder maxSubsumes $binder1 = $result", capt) case _ => true case _ => - this.isCap && canAddHidden && vs != VarState.HardSeparate + this.isCap /*&& !yIsExistential*/ && canAddHidden && vs != VarState.HardSeparate || y.match case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f0b73998d368..8637fe1b8fe0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1260,7 +1260,7 @@ class CheckCaptures extends Recheck, SymTransformer: if isCompatible(actualBoxed, expected1) then if debugSuccesses then tree match case Ident(_) => - println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}") + println(i"SUCCESS $tree for $actual <:< $expected:\n${TypeComparer.explained(_.isSubType(actualBoxed, expected1))}") case _ => actualBoxed else diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 253dd3791daf..17aa9898da4e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -281,7 +281,8 @@ object Existential: seen.getOrElseUpdate(t, Vble(mt)) else if variance == 0 then - fail(em"""$tp captures the root capability `cap` in invariant position""") + fail(em"""$tp captures the root capability `cap` in invariant position. + |This capability cannot be converted to an existential in the result type of a function.""") // we accept variance < 0, and leave the cap as it is super.mapOver(t) case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index b4ad91343efb..c5e55f5842c7 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -403,7 +403,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: && (!sym.isConstructor || (t ne tp.finalResultType)) // Don't add ^ to result types of class constructors deriving from Capability then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) - else normalizeCaptures(normalizeFunctions(mapFollowingAliases(t), t)) + else normalizeCaptures(mapFollowingAliases(t)) def innerApply(t: Type) = t match @@ -438,9 +438,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: keptFunAliases = true mapOver(t) else - // In the second pass, map the alias and make sure it has the form - // of a dependent function. - normalizeFunctions(apply(t.dealias), t, expandAlways = true) + // In the second pass, map the alias + apply(t.dealias) case t => defaultApply(t) end toCapturing diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index f4f9ac6b97a4..f8b0675bd204 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -321,10 +321,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // This is safe because X$ self-type is X.type sym1 = sym1.companionModule if (sym1 ne NoSymbol) && (sym1 eq sym2) then - ctx.erasedTypes || - sym1.isStaticOwner || - isSubPrefix(tp1.prefix, tp2.prefix) || - thirdTryNamed(tp2) + ctx.erasedTypes + || sym1.isStaticOwner + || isSubPrefix(tp1.prefix, tp2.prefix) + || thirdTryNamed(tp2) else (tp1.name eq tp2.name) && !sym1.is(Private) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 866d9ba21cb4..1eecdd86cb0c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1124,7 +1124,7 @@ object Types extends TypeUtils { TypeComparer.topLevelSubType(this, that) } - /** Is this type a subtype of that type? */ + /** Is this type a subtype of that type without adding to the constraint? */ final def frozen_<:<(that: Type)(using Context): Boolean = { record("frozen_<:<") TypeComparer.isSubTypeWhenFrozen(this, that) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index f5bdbc19af69..a348c3ed0b23 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -260,7 +260,7 @@ class PlainPrinter(_ctx: Context) extends Printer { def isUniversal = refs.elems.size == 1 && (refs.isUniversal - || !printDebug && !showUniqueIds && refs.elems.nth(0).match + || !printDebug && !printFresh && !showUniqueIds && refs.elems.nth(0).match case Existential.Vble(binder) => CCState.openExistentialScopes match case b :: _ => binder eq b @@ -459,7 +459,7 @@ class PlainPrinter(_ctx: Context) extends Printer { val vbleText: Text = CCState.openExistentialScopes.indexOf(binder) match case -1 => "" - case n => "outer_" * n ++ "cap" + case n => "outer_" * n ++ (if printFresh then "localcap" else "cap") vbleText ~ hashStr(binder) case Fresh(hidden) => val idStr = if showUniqueIds then s"#${hidden.id}" else "" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index fef0bee16465..5839a7a817c8 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -190,8 +190,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { val capturesRoot = refs == rootSetText if cc.isCaptureCheckingOrSetup && tp.allParamNamesSynthetic && !tp.looksDependent - && !showUniqueIds && !printDebug - then + && !showUniqueIds && !printDebug && !printFresh + then // cc.Setup converts all functions to dependent functions. Undo that when printing. toTextFunction(tp.paramInfos, tp.resType, tp, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) else diff --git a/tests/neg-custom-args/captures/boundschecks3.check b/tests/neg-custom-args/captures/boundschecks3.check index 36e1336e8f05..035d327e3d71 100644 --- a/tests/neg-custom-args/captures/boundschecks3.check +++ b/tests/neg-custom-args/captures/boundschecks3.check @@ -1,4 +1,24 @@ --- Error: tests/neg-custom-args/captures/boundschecks3.scala:11:13 ----------------------------------------------------- -11 | val bar: T -> T = ??? // error, since `T` is expanded here. But the msg is not very good. - | ^^^^^^ - | test.C[box test.Tree^] captures the root capability `cap` in invariant position +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:9:11 --------------------------------- +9 | val foo: C[Tree^] = ??? // error + | ^ + | Type argument box test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[box test.Tree^] + | + | longer explanation available when compiling with `-explain` +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:10:11 -------------------------------- +10 | type T = C[Tree^] // error + | ^ + | Type argument box test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[box test.Tree^] + | + | longer explanation available when compiling with `-explain` +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:11:11 -------------------------------- +11 | val bar: T -> T = ??? // error + | ^ + |Type argument box test.Tree^ does not conform to upper bound test.Tree in subpart test.C[box test.Tree^] of inferred type test.C[box test.Tree^] -> test.C[box test.Tree^] + | + | longer explanation available when compiling with `-explain` +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:12:11 -------------------------------- +12 | val baz: C[Tree^] -> Unit = ??? // error + | ^ + |Type argument box test.Tree^ does not conform to upper bound test.Tree in subpart test.C[box test.Tree^] of inferred type test.C[box test.Tree^] -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/boundschecks3.scala b/tests/neg-custom-args/captures/boundschecks3.scala index f5e9652c0913..72a562e3628b 100644 --- a/tests/neg-custom-args/captures/boundschecks3.scala +++ b/tests/neg-custom-args/captures/boundschecks3.scala @@ -6,8 +6,8 @@ object test { class C[X <: Tree](x: X) - val foo: C[Tree^] = ??? // hidden error - type T = C[Tree^] // hidden error - val bar: T -> T = ??? // error, since `T` is expanded here. But the msg is not very good. - val baz: C[Tree^] -> Unit = ??? // hidden error + val foo: C[Tree^] = ??? // error + type T = C[Tree^] // error + val bar: T -> T = ??? // error + val baz: C[Tree^] -> Unit = ??? // error } diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.check b/tests/neg-custom-args/captures/cc-existential-conformance.check index 7fa04c93fe3f..31ffea11a48c 100644 --- a/tests/neg-custom-args/captures/cc-existential-conformance.check +++ b/tests/neg-custom-args/captures/cc-existential-conformance.check @@ -1,7 +1,20 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:8:24 -------------------- 8 | val y: A -> Fun[B^] = x // error | ^ - | Found: (x : A -> A -> B^) - | Required: A -> A -> B^{outer_cap} + | Found: (x : A -> (x²: A) -> B^{localcap}) + | Required: A -> A -> B^{fresh} + | + | where: x is a value in method test + | x² is a reference to a value parameter | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:13:19 ------------------- +13 | val y: Fun[B^] = x // error + | ^ + | Found: (x : (x²: A) -> B^{localcap}) + | Required: A -> B^{fresh} + | + | where: x is a value in method test2 + | x² is a reference to a value parameter + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.scala b/tests/neg-custom-args/captures/cc-existential-conformance.scala index 9c602aebac9a..61bbe5ddde6a 100644 --- a/tests/neg-custom-args/captures/cc-existential-conformance.scala +++ b/tests/neg-custom-args/captures/cc-existential-conformance.scala @@ -4,11 +4,11 @@ class B type Fun[T] = A -> T def test() = - val x: A -> A -> B^ = ??? + val x: A -> (x: A) -> B^ = ??? val y: A -> Fun[B^] = x // error val z: A -> A -> B^ = y // ok def test2() = - val x: A -> B^ = ??? - val y: Fun[B^] = x // should be error + val x: (x: A) -> B^ = ??? + val y: Fun[B^] = x // error val z: A -> B^ = y // ok diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index 243b9fd56db7..5ffb0873d752 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -1,15 +1,15 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:19:4 ---------------------------------- 19 | op // error | ^^ - | Found: (xs: List[(X, box () ->{io} Unit)]) ->{op} List[box () ->{xs*} Unit] + | Found: (op : (xs: List[(X, box () ->{io} Unit)]) => List[box () ->{xs*} Unit]) | Required: (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:26:60 --------------------------------- 26 | val b: (xs: List[() ->{io} Unit]) => List[() ->{} Unit] = a // error | ^ - | Found: (xs: List[box () ->{io} Unit]) ->{a} List[box () ->{xs*} Unit] - | Required: (xs: List[box () ->{io} Unit]) => List[() -> Unit] + | Found: (a : (xs: List[box () ->{io} Unit]) => List[box () ->{xs*} Unit]) + | Required: (xs: List[box () ->{io} Unit]) => List[() -> Unit] | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/depfun-reach.scala:18:17 ------------------------------------------------------ diff --git a/tests/neg-custom-args/captures/existential-mapping.check b/tests/neg-custom-args/captures/existential-mapping.check index 3ed5f0808b9f..974c5c38f74c 100644 --- a/tests/neg-custom-args/captures/existential-mapping.check +++ b/tests/neg-custom-args/captures/existential-mapping.check @@ -1,88 +1,89 @@ --- Error: tests/neg-custom-args/captures/existential-mapping.scala:44:13 ----------------------------------------------- -44 | val z1: A^ => Array[C^] = ??? // error - | ^^^^^^^^^^^^^^^ - | Array[box C^] captures the root capability `cap` in invariant position +-- Error: tests/neg-custom-args/captures/existential-mapping.scala:46:10 ----------------------------------------------- +46 | val z2: (x: A^) => Array[C^] = ??? // error + | ^^^^^^^^^^^^^^^^^^^^ + | Array[box C^] captures the root capability `cap` in invariant position. + | This capability cannot be converted to an existential in the result type of a function. -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:9:25 --------------------------- 9 | val _: (x: C^) -> C = x1 // error | ^^ - | Found: (x1 : (x: C^) -> C^) + | Found: (x1 : (x: C^) -> C^{localcap}) | Required: (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:12:20 -------------------------- 12 | val _: C^ -> C = x2 // error | ^^ - | Found: (x2 : C^ -> C^) + | Found: (x2 : C^ -> C^{fresh}) | Required: C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:15:30 -------------------------- 15 | val _: A^ -> (x: C^) -> C = x3 // error | ^^ - | Found: (x3 : A^ -> (x: C^) -> C^) + | Found: (x3 : A^ -> (x: C^) -> C^{localcap}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:18:25 -------------------------- 18 | val _: A^ -> C^ -> C = x4 // error | ^^ - | Found: (x4 : A^ -> C^ -> C^) + | Found: (x4 : A^ -> C^ -> C^{fresh}) | Required: A^ -> C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:21:30 -------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (x: C^) -> C^) + | Found: (x5 : A^ -> (x: C^) -> C^{localcap}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:24:30 -------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (x: C^) => C^) - | Required: A^ -> (x: C^) => C + | Found: (x6 : A^ -> (x: C^) ->{fresh} C^{localcap}) + | Required: A^ -> (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:27:25 -------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) ->{fresh} C^) + | Found: (y1 : (x: C^) ->{fresh} C^{localcap}) | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:30:20 -------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ ->{fresh} C^) + | Found: (y2 : C^ ->{fresh} C^{fresh}) | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:33:30 -------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ ->{fresh} (x: C^) => C^) - | Required: A^ ->{fresh} (x: C^) => C + | Found: (y3 : A^ ->{fresh} (x: C^) ->{fresh} C^{localcap}) + | Required: A^ ->{fresh} (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:36:25 -------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ ->{fresh} C^ => C^) - | Required: A^ ->{fresh} C^ => C + | Found: (y4 : A^ ->{fresh} C^ ->{fresh} C^{fresh}) + | Required: A^ ->{fresh} C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:39:30 -------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ ->{fresh} (x: C^) -> C^) + | Found: (y5 : A^ ->{fresh} (x: C^) -> C^{localcap}) | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:42:30 -------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ ->{fresh} (x: C^) => C^) - | Required: A^ ->{fresh} (x: C^) => C + | Found: (y6 : A^ ->{fresh} (x: C^) ->{fresh} C^{localcap}) + | Required: A^ ->{fresh} (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/existential-mapping.scala b/tests/neg-custom-args/captures/existential-mapping.scala index 290f7dc767a6..3543ef27037f 100644 --- a/tests/neg-custom-args/captures/existential-mapping.scala +++ b/tests/neg-custom-args/captures/existential-mapping.scala @@ -41,6 +41,7 @@ def Test = val y6: A^ => IFun[C^] = ??? val _: A^ => (x: C^) => C = y6 // error - val z1: A^ => Array[C^] = ??? // error + val z1: A^ => Array[C^] = ??? // ok + val z2: (x: A^) => Array[C^] = ??? // error diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index 0786809d7c07..646e40fb10cb 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -8,7 +8,7 @@ -- [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*} box Logger{val f: File^{_$1}}^{cap.rd, _$1} + | Found: (_$1: box File^{files*}) ->{files*} box Logger{val f: File^{_$1}}^{localcap.rd, _$1} | Required: (_$1: box File^{files*}) ->{fresh} box Logger{val f: File^?}^? | | Note that reference .rd diff --git a/tests/neg-custom-args/captures/leaked-curried.check b/tests/neg-custom-args/captures/leaked-curried.check index 23726db62508..9199d468b55a 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -8,3 +8,11 @@ | ^^ | reference (io : Cap^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> () ->{io} Cap^ +-- Error: tests/neg-custom-args/captures/leaked-curried.scala:13:15 ---------------------------------------------------- +13 | val get: () ->{} () ->{io} Cap^ = // error: separation + | ^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: value get's type () -> () ->{io} Cap^ hides non-local parameter io +-- Error: tests/neg-custom-args/captures/leaked-curried.scala:16:15 ---------------------------------------------------- +16 | val get: () ->{} () ->{io} Cap^ = // error: separation + | ^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: value get's type () -> () ->{io} Cap^ hides non-local parameter io diff --git a/tests/neg-custom-args/captures/leaked-curried.scala b/tests/neg-custom-args/captures/leaked-curried.scala index d765955ee6ce..576c9d8a5db9 100644 --- a/tests/neg-custom-args/captures/leaked-curried.scala +++ b/tests/neg-custom-args/captures/leaked-curried.scala @@ -10,10 +10,11 @@ def main(): Unit = val leaked = withCap: (io: Cap^) => class Fuzz extends Box, Pure: self => - val get: () ->{} () ->{io} Cap^ = + val get: () ->{} () ->{io} Cap^ = // error: separation () => () => io // error class Foo extends Box, Pure: - val get: () ->{} () ->{io} Cap^ = + val get: () ->{} () ->{io} Cap^ = // error: separation () => () => io // error new Foo val bad = leaked.get()().use() // using a leaked capability + diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 0524d4154aa3..f4fd189ce529 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -45,24 +45,31 @@ | Type variable A of constructor Id cannot be instantiated to box () => Unit since | that type captures the root capability `cap`. -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:63:27 -------------------------------------- -63 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error +63 | val f1: File^{id*} = id(f) // error // error | ^^^^^ | Found: File^{f} | Required: File^{id*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:80:10 ----------------------------------------------------------- -80 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:71:27 -------------------------------------- +71 | val f1: File^{id*} = id(f) // error // error + | ^^^^^ + | Found: File^{f} + | Required: File^{id*} + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/reaches.scala:88:10 ----------------------------------------------------------- +88 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/reaches.scala:80:13 ----------------------------------------------------------- -80 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck +-- Error: tests/neg-custom-args/captures/reaches.scala:88:13 ----------------------------------------------------------- +88 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/reaches.scala:80:28 ----------------------------------------------------------- -80 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck +-- Error: tests/neg-custom-args/captures/reaches.scala:88:28 ----------------------------------------------------------- +88 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Separation failure: argument of type A ->{x} box A^? | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C @@ -75,8 +82,8 @@ | Capture set of second argument : {y} | Footprint set of second argument : {y, ps*} | The two sets overlap at : {ps*} --- Error: tests/neg-custom-args/captures/reaches.scala:83:28 ----------------------------------------------------------- -83 | ps.map((x, y) => compose1(x, y)) // error sepcheck +-- Error: tests/neg-custom-args/captures/reaches.scala:91:28 ----------------------------------------------------------- +91 | ps.map((x, y) => compose1(x, y)) // error sepcheck | ^ | Separation failure: argument of type A ->{x} box A^? | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C @@ -94,6 +101,14 @@ | ^^^ | id* cannot be tracked since its deep capture set is empty -- Error: tests/neg-custom-args/captures/reaches.scala:63:18 ----------------------------------------------------------- -63 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error +63 | val f1: File^{id*} = id(f) // error // error + | ^^^ + | id* cannot be tracked since its deep capture set is empty +-- Error: tests/neg-custom-args/captures/reaches.scala:70:31 ----------------------------------------------------------- +70 | val leaked = usingFile[File^{id*}]: f => // error + | ^^^ + | id* cannot be tracked since its deep capture set is empty +-- Error: tests/neg-custom-args/captures/reaches.scala:71:18 ----------------------------------------------------------- +71 | val f1: File^{id*} = id(f) // error // error | ^^^ | id* cannot be tracked since its deep capture set is empty diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index 9f03e0659d46..f8689819f915 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -57,10 +57,18 @@ def test = def attack2 = val id: File^ -> File^ = x => x + // val id: File^ -> File^{fresh} + + val leaked = usingFile[File^{id*}]: f => // error + val f1: File^{id*} = id(f) // error // error + f1 + +def attack3 = + val id: (x: File^) -> File^ = x => x // val id: File^ -> EX C.File^C val leaked = usingFile[File^{id*}]: f => // error - val f1: File^{id*} = id(f) // error, since now id(f): File^ // error + val f1: File^{id*} = id(f) // error // error f1 class List[+A]: diff --git a/tests/neg-custom-args/captures/sep-use2.check b/tests/neg-custom-args/captures/sep-use2.check index e7f5383a497f..a8e3aa884fd5 100644 --- a/tests/neg-custom-args/captures/sep-use2.check +++ b/tests/neg-custom-args/captures/sep-use2.check @@ -27,8 +27,8 @@ 18 | { f(cc) } // error // error | ^^ | Separation failure: argument of type (cc : -> Object^) - | to a function of type Object^ ->{c} Object^ - | corresponds to capture-polymorphic formal parameter x$0 of type Object^ + | to a function of type (x: Object^) ->{c} Object^ + | corresponds to capture-polymorphic formal parameter x of type Object^ | and hides capabilities {cap, c}. | Some of these overlap with the captures of the function prefix. | @@ -49,3 +49,17 @@ | Separation failure: Illegal access to {c} which is hidden by the previous definition | of method cc with result type Object^. | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:24:8 ----------------------------------------------------------- +24 | { f(c) } // error + | ^ + | Separation failure: argument of type (c : Object^) + | to a function of type Object^ ->{c} Object^ + | corresponds to capture-polymorphic formal parameter v1 of type Object^ + | and hides capabilities {c}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {c} + | Hidden footprint of current argument : {c} + | Capture set of function prefix : {f} + | Footprint set of function prefix : {f, c} + | The two sets overlap at : {c} diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala index 48f2a84c6fe4..a31fb098a719 100644 --- a/tests/neg-custom-args/captures/sep-use2.scala +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -1,7 +1,7 @@ import caps.consume -def test1(@consume c: Object^, f: Object^ => Object^) = +def test1(@consume c: Object^, f: (x: Object^) => Object^) = def cc: Object^ = c // error val x1 = { f(cc) } // ok @@ -12,13 +12,19 @@ def test1(@consume c: Object^, f: Object^ => Object^) = val x4: Object^ = // error { f(c) } // error -def test2(@consume c: Object^, f: Object^ ->{c} Object^) = +def test2(@consume c: Object^, f: (x: Object^) ->{c} Object^) = def cc: Object^ = c // error val x1 = { f(cc) } // error // error - val x4: Object^ = + val x4: Object^ = // ^ hides just c, since the Object^ in the result of `f` is existential { f(c) } // error // error +def test3(@consume c: Object^, f: Object^ ->{c} Object^) = + val x4: Object^ = // ^ hides c and f* + { f(c) } // error + + + diff --git a/tests/pos-custom-args/captures/list-encoding.scala b/tests/pos-custom-args/captures/list-encoding.scala index d959b523404b..7c593bb524e3 100644 --- a/tests/pos-custom-args/captures/list-encoding.scala +++ b/tests/pos-custom-args/captures/list-encoding.scala @@ -4,7 +4,7 @@ import annotation.retains class Cap type Op[T, C] = - (v: T) => (s: C) => C + T => C => C type List[T] = [C] -> (op: Op[T, C]) -> (s: C) ->{op} C diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala index 74438aa5793a..a9ff6e258317 100644 --- a/tests/pos-custom-args/captures/skolems2.scala +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -2,7 +2,7 @@ import caps.consume import caps.unsafe.unsafeAssumeSeparate -def Test(@consume c: Object^, f: Object^ => Object^) = +def Test(@consume c: Object^, @consume f: Object^ => Object^) = def cc: Object^ = unsafeAssumeSeparate(c) val x1 = { f(cc) } @@ -11,7 +11,7 @@ def Test(@consume c: Object^, f: Object^ => Object^) = val x3: Object^ = f(cc) val x4: Object^ = - { f(cc) } + { unsafeAssumeSeparate(f)(cc) } From b17ef263a7f3a140600740424e3b324a24b15171 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 4 Mar 2025 10:03:13 +0100 Subject: [PATCH 67/93] Tighten subsume rules for existentials 1. cap no longer subsumes existential variables 2. existential variables don't subsume other existential variables Still open: Existential variables should only subsume other capabilties if these are sharable capabilities. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 9 --------- compiler/src/dotty/tools/dotc/cc/CaptureRef.scala | 5 ++--- tests/neg-custom-args/captures/unsound-reach-3.scala | 6 +++--- tests/neg-custom-args/captures/unsound-reach-4.check | 4 ++-- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index cd3645f0b1f6..3b18f94b3f97 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -584,15 +584,6 @@ extension (tp: Type) case tp: ThisType => tp.cls.ccLevel.nextInner case _ => undefinedLevel - /** Is this a method or function that has `other` as its direct or indirect result - * type? - */ - def hasSuffix(other: MethodType)(using Context): Boolean = - (tp eq other) || tp.match - case defn.RefinedFunctionOf(mt) => mt.hasSuffix(other) - case mt: MethodType => mt.resType.hasSuffix(other) - case _ => false - extension (tp: MethodType) /** A method marks an existential scope unless it is the prefix of a curried method */ def marksExistentialScope(using Context): Boolean = diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 3fa524933f67..98e96b414754 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -264,11 +264,10 @@ trait CaptureRef extends TypeProxy, ValueType: || !y.stripReadOnly.isCap && !yIsExistential && canAddHidden && vs.addHidden(hidden, y) case Existential.Vble(binder) => y.stripReadOnly match - case Existential.Vble(binder1) => binder1.hasSuffix(binder) - .showing(i"cmp existential $binder maxSubsumes $binder1 = $result", capt) + case Existential.Vble(binder1) => false case _ => true case _ => - this.isCap /*&& !yIsExistential*/ && canAddHidden && vs != VarState.HardSeparate + this.isCap && !yIsExistential && canAddHidden && vs != VarState.HardSeparate || y.match case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) case _ => false diff --git a/tests/neg-custom-args/captures/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala index 0aeb10e39916..46ed469a308e 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -10,7 +10,7 @@ def withFile[R](path: String)(op: File^ => R): R = ??? trait Foo[+X]: def use(@consume x: File^): X class Bar extends Foo[File^]: // error - def use(@consume x: File^): File^ = x + def use(@consume x: File^): File^ = x // error override def bad(): Unit = val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). @@ -18,7 +18,7 @@ def bad(): Unit = var escaped: File^{backdoor*} = null withFile("hello.txt"): f => - escaped = boom.use(f) // error - // boom.use: (x: File^) -> File^{backdoor*}, it is a selection so reach capabilities are allowed + escaped = boom.use(f) // error: separation + // was boom.use: (x: File^) -> File^{backdoor*}, it is a selection so reach capabilities are allowed // f: File^, so there is no reach capabilities diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index c7323b5aeda9..f21d1403eb3d 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -13,7 +13,7 @@ -- [E164] Declaration Error: tests/neg-custom-args/captures/unsound-reach-4.scala:17:6 --------------------------------- 17 | def use(@consume x: F): File^ = x // error @consume override | ^ - |error overriding method use in trait Foo of type (x: File^): box File^; - | method use of type (x: File^): File^ has a parameter x with different @consume status than the corresponding parameter in the overridden definition + | error overriding method use in trait Foo of type (x: File^): box File^; + | method use of type (x: File^): File^ has incompatible type | | longer explanation available when compiling with `-explain` From d2d529e4a26203a39bdf32485723b2bd5a5f9976 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 4 Mar 2025 12:17:16 +0100 Subject: [PATCH 68/93] Tighten existential subsume rules further: Also drop existential instances subsuming Fresh instances. --- compiler/src/dotty/tools/dotc/cc/CaptureRef.scala | 1 + .../captures/cc-existential-conformance.check | 14 ++++++++++++++ .../captures/cc-existential-conformance.scala | 4 ++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 98e96b414754..3ca7e05a4c5d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -265,6 +265,7 @@ trait CaptureRef extends TypeProxy, ValueType: case Existential.Vble(binder) => y.stripReadOnly match case Existential.Vble(binder1) => false + case Fresh(_) => false case _ => true case _ => this.isCap && !yIsExistential && canAddHidden && vs != VarState.HardSeparate diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.check b/tests/neg-custom-args/captures/cc-existential-conformance.check index 31ffea11a48c..ba82acdcd47e 100644 --- a/tests/neg-custom-args/captures/cc-existential-conformance.check +++ b/tests/neg-custom-args/captures/cc-existential-conformance.check @@ -8,6 +8,13 @@ | x² is a reference to a value parameter | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:9:29 -------------------- +9 | val z: A -> (x: A) -> B^ = y // error + | ^ + | Found: (y : A -> A -> B^) + | Required: A -> (x: A) -> B^ + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:13:19 ------------------- 13 | val y: Fun[B^] = x // error | ^ @@ -18,3 +25,10 @@ | x² is a reference to a value parameter | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:14:24 ------------------- +14 | val z: (x: A) -> B^ = y // error + | ^ + | Found: (y : A -> B^) + | Required: (x: A) -> B^ + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.scala b/tests/neg-custom-args/captures/cc-existential-conformance.scala index 61bbe5ddde6a..107570cc5019 100644 --- a/tests/neg-custom-args/captures/cc-existential-conformance.scala +++ b/tests/neg-custom-args/captures/cc-existential-conformance.scala @@ -6,9 +6,9 @@ type Fun[T] = A -> T def test() = val x: A -> (x: A) -> B^ = ??? val y: A -> Fun[B^] = x // error - val z: A -> A -> B^ = y // ok + val z: A -> (x: A) -> B^ = y // error def test2() = val x: (x: A) -> B^ = ??? val y: Fun[B^] = x // error - val z: A -> B^ = y // ok + val z: (x: A) -> B^ = y // error From f3ef42bf0f05b9c12d51ff45242e3654702a3286 Mon Sep 17 00:00:00 2001 From: Hamza Remmal Date: Wed, 5 Mar 2025 00:23:24 +0100 Subject: [PATCH 69/93] chore: double the heap size and the stack size --- .jvmopts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.jvmopts b/.jvmopts index a50abf36aa42..4df4f826d1db 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1,5 +1,5 @@ -Xss1m --Xms512m --Xmx4096m +-Xms1024m +-Xmx8192m -XX:MaxInlineLevel=35 -XX:ReservedCodeCacheSize=512m From 4c53fd14d6919b54e837865aafb78cfb338aa306 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Mar 2025 11:41:22 +0100 Subject: [PATCH 70/93] Allow whitespace between -> and following capture set Not allowing whitespace clashes with the rules for leading infix operators. For instance, the following does not work, since `->` is not recognized as a leading infix operator. ```scala A ->{x} B ``` Also: Print existential capture set id under -uniqids --- .../dotty/tools/dotc/parsing/Parsers.scala | 2 +- .../src/dotty/tools/dotc/parsing/Tokens.scala | 2 ++ .../tools/dotc/printing/PlainPrinter.scala | 5 +-- tests/pending/pos/classcaps.scala | 19 +++++++++++ .../captures/erased-methods2.scala | 34 +++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 tests/pending/pos/classcaps.scala create mode 100644 tests/pos-custom-args/captures/erased-methods2.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c61c2703eb1a..39ad8fc9a5e8 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1638,7 +1638,7 @@ object Parsers { } def capturesAndResult(core: () => Tree): Tree = - if Feature.ccEnabled && in.token == LBRACE && in.offset == in.lastOffset + if Feature.ccEnabled && in.token == LBRACE && canStartCaptureSetContentsTokens.contains(in.lookahead.token) then CapturesAndResult(captureSet(), core()) else core() diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index c78a336ecdf5..bc55371ec96a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -295,6 +295,8 @@ object Tokens extends TokensCommon { final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, NEW) + final val canStartCaptureSetContentsTokens = BitSet(IDENTIFIER, BACKQUOTED_IDENT, THIS, RBRACE) + final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index a348c3ed0b23..bacc357d1474 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -454,13 +454,14 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" - case Existential.Vble(binder) => + case tp @ Existential.Vble(binder) => + val idStr = s"##${tp.annot.asInstanceOf[Fresh.Annot].hidden.id}" // TODO: Better printing? USe a mode where we print more detailed val vbleText: Text = CCState.openExistentialScopes.indexOf(binder) match case -1 => "" case n => "outer_" * n ++ (if printFresh then "localcap" else "cap") - vbleText ~ hashStr(binder) + vbleText ~ hashStr(binder) ~ Str(idStr).provided(showUniqueIds) case Fresh(hidden) => val idStr = if showUniqueIds then s"#${hidden.id}" else "" if printFreshDetailed then s"" diff --git a/tests/pending/pos/classcaps.scala b/tests/pending/pos/classcaps.scala new file mode 100644 index 000000000000..612a740c8c42 --- /dev/null +++ b/tests/pending/pos/classcaps.scala @@ -0,0 +1,19 @@ + +import language.experimental.captureChecking +import caps.* + +case class A() + +trait HasCap: + def mkA: A^{this} + +object HasCap: + def apply[T](body: HasCap^ ?=> T): T = ??? + +class Box(using h: HasCap^): + var t: A^{h} = h.mkA + +def main() = + HasCap: h ?=> + val b = Box(using h) + b.t = h.mkA \ No newline at end of file diff --git a/tests/pos-custom-args/captures/erased-methods2.scala b/tests/pos-custom-args/captures/erased-methods2.scala new file mode 100644 index 000000000000..8f1d8897d61a --- /dev/null +++ b/tests/pos-custom-args/captures/erased-methods2.scala @@ -0,0 +1,34 @@ +import language.experimental.saferExceptions +import language.experimental.erasedDefinitions +import language.experimental.captureChecking + +class Ex1 extends Exception("Ex1") +class Ex2 extends Exception("Ex2") +class Ex3 extends Exception("Ex3") + +erased class CT[-E <: Exception] extends caps.Capability + +def Throw[Ex <: Exception](ex: Ex)(using CT[Ex]^): Nothing = ??? + +def foo8a(i: Int) = + (erased xx1: CT[Ex2]^) ?=> Throw(new Ex2) + +def foo9a(i: Int) + : (x$1: CT[Ex3]^) + ?=> (x$2: CT[Ex2]^) + ?-> {x$1, caps.cap} Unit + = (x$1: CT[Ex3]^) + ?=> (x$2: CT[Ex2]^) + ?=> + //given (CT[Ex3]^) = x$1 + Throw(new Ex3) + +def foo10a(i: Int) + : (erased x$0: CT[Ex3]^) + ?=> (erased x$1: CT[Ex2]^) + ?-> {x$0, caps.cap} (erased x$2: CT[Ex1]^) + ?-> {x$0, x$1, caps.cap} Unit + = (erased x$1: CT[Ex3]^) + ?=> (erased x$2: CT[Ex2]^) + ?=> (erased x$3: CT[Ex1]^) + ?=> Throw(new Ex3) From 641dffa2412093ab0c858f83c54eb9d4b9242cfc Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Mar 2025 11:42:28 +0100 Subject: [PATCH 71/93] Make CanThrow a SharedCapability --- library/src/scala/CanThrow.scala | 2 +- .../pos-custom-args/captures/erased-methods.scala | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index 91c94229c43c..485dcecb37df 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -8,7 +8,7 @@ import annotation.{implicitNotFound, experimental, capability} */ @experimental @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") -erased class CanThrow[-E <: Exception] extends caps.Capability +erased class CanThrow[-E <: Exception] extends caps.SharedCapability @experimental object unsafeExceptions: diff --git a/tests/pos-custom-args/captures/erased-methods.scala b/tests/pos-custom-args/captures/erased-methods.scala index 911c779e08e5..43347c6d9172 100644 --- a/tests/pos-custom-args/captures/erased-methods.scala +++ b/tests/pos-custom-args/captures/erased-methods.scala @@ -7,14 +7,14 @@ class Ex2 extends Exception("Ex2") class Ex3 extends Exception("Ex3") def foo8a(i: Int) = - (erased xx1: CanThrow[Ex2]^) ?=> throw new Ex2 + (erased xx1: CanThrow[Ex2]) ?=> throw new Ex2 def foo9a(i: Int) - : (erased x$0: CanThrow[Ex3]^) - ?=> (erased x$1: CanThrow[Ex2]^) - ?=> (erased x$2: CanThrow[Ex1]^) + : (erased x$0: CanThrow[Ex3]) + ?=> (erased x$1: CanThrow[Ex2]) + ?=> (erased x$2: CanThrow[Ex1]) ?=> Unit - = (erased x$1: CanThrow[Ex3]^) - ?=> (erased x$2: CanThrow[Ex2]^) - ?=> (erased x$3: CanThrow[Ex1]^) + = (erased x$1: CanThrow[Ex3]) + ?=> (erased x$2: CanThrow[Ex2]) + ?=> (erased x$3: CanThrow[Ex1]) ?=> throw new Ex3 From 58baf53149075a90722ad644616cadae9a921ca0 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Mar 2025 15:46:00 +0100 Subject: [PATCH 72/93] Let existential variables only subsume shared capabilities Also, implement infrastructure to add a note to a type mismatch when a failure to subsume occurs. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 6 +++ .../src/dotty/tools/dotc/cc/CaptureRef.scala | 9 +++-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 19 ++++++++- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 10 ++++- .../captures/cc-existential-conformance.check | 6 +++ .../captures/erased-methods2.check | 29 ++++++++++++++ .../captures/erased-methods2.scala | 10 ++--- .../captures/heal-tparam-cs.check | 39 +++++++++++++++++++ .../captures/heal-tparam-cs.scala | 2 +- tests/neg-custom-args/captures/reaches.check | 10 +++++ tests/neg-custom-args/captures/reaches.scala | 2 +- 11 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 tests/neg-custom-args/captures/erased-methods2.check rename tests/{pos-custom-args => neg-custom-args}/captures/erased-methods2.scala (81%) create mode 100644 tests/neg-custom-args/captures/heal-tparam-cs.check diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 3b18f94b3f97..5aa6fb1f86a0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -84,6 +84,12 @@ class CCState: */ var levelError: Option[CaptureSet.CompareResult.LevelError] = None + /** Optionally, a pair of an existential variable and another capability. + * Set when a subsumes check decides that an existential variable cannot be + * instantiated to the other capability. + */ + var existentialSubsumesFailure: Option[(CaptureRef, CaptureRef)] = None + /** Warnings relating to upper approximations of capture sets with * existentially bound variables. */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 3ca7e05a4c5d..121fa5a5a979 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -263,10 +263,11 @@ trait CaptureRef extends TypeProxy, ValueType: vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) || !y.stripReadOnly.isCap && !yIsExistential && canAddHidden && vs.addHidden(hidden, y) case Existential.Vble(binder) => - y.stripReadOnly match - case Existential.Vble(binder1) => false - case Fresh(_) => false - case _ => true + if y.derivesFromSharedCapability then true + else + ccState.existentialSubsumesFailure = + ccState.existentialSubsumesFailure.orElse(Some(this, y)) + false case _ => this.isCap && !yIsExistential && canAddHidden && vs != VarState.HardSeparate || y.match diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 8637fe1b8fe0..801800c7ffed 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1218,6 +1218,19 @@ class CheckCaptures extends Recheck, SymTransformer: | |Note that ${msg.toString}""" + private def existentialSubsumesFailureAddenda(using Context): Addenda = + ccState.existentialSubsumesFailure match + case Some((ex @ Existential.Vble(binder), other)) => + new Addenda: + override def toAdd(using Context): List[String] = + val ann = ex.annot.asInstanceOf[Fresh.Annot] + i""" + | + |Note that the existential capture root in ${ann.originalBinder.resType} + |cannot subsume the capability $other""" + :: Nil + case _ => NothingToAdd + /** Addendas for error messages that show where we have under-approximated by * mapping a a capture ref in contravariant position to the empty set because * the original result type of the map was not itself a capture ref. @@ -1257,6 +1270,7 @@ class CheckCaptures extends Recheck, SymTransformer: if actualBoxed eq actual then // Only `addOuterRefs` when there is no box adaptation expected1 = addOuterRefs(expected1, actual, tree.srcPos) + ccState.existentialSubsumesFailure = None if isCompatible(actualBoxed, expected1) then if debugSuccesses then tree match case Ident(_) => @@ -1268,7 +1282,10 @@ class CheckCaptures extends Recheck, SymTransformer: inContext(Fresh.printContext(actualBoxed, expected1)): err.typeMismatch(tree.withType(actualBoxed), expected1, addApproxAddenda( - addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), + addenda + ++ CaptureSet.levelErrors + ++ boxErrorAddenda(boxErrors) + ++ existentialSubsumesFailureAddenda, 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 5a88673ae4af..7427a7350e8f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -31,8 +31,15 @@ object Fresh: override def tree(using Context) = New(symbol.typeRef, Nil) override def derivedAnnotation(tree: Tree)(using Context): Annotation = this + private var myOriginalBinder = binder + def originalBinder: MethodType = myOriginalBinder.asInstanceOf[MethodType] + def derivedAnnotation(binder: MethodType | NoType.type)(using Context): Annotation = - if this.binder eq binder then this else Annot(hidden, binder) + if this.binder eq binder then this + else + val ann = Annot(hidden, binder) + ann.myOriginalBinder = myOriginalBinder + ann override def hash: Int = hidden.hashCode override def eql(that: Annotation) = that match @@ -70,6 +77,7 @@ object Fresh: val hiddenSet = CaptureSet.HiddenSet(NoSymbol) val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet, binder)) hiddenSet.owningCap = res + //assert(hiddenSet.id != 9, binder.show) res /** The initial elements (either 0 or 1) of a hidden set created for given `owner`. diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.check b/tests/neg-custom-args/captures/cc-existential-conformance.check index ba82acdcd47e..bc94b97fc327 100644 --- a/tests/neg-custom-args/captures/cc-existential-conformance.check +++ b/tests/neg-custom-args/captures/cc-existential-conformance.check @@ -14,6 +14,9 @@ | Found: (y : A -> A -> B^) | Required: A -> (x: A) -> B^ | + | Note that the existential capture root in B^ + | cannot subsume the capability cap + | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:13:19 ------------------- 13 | val y: Fun[B^] = x // error @@ -31,4 +34,7 @@ | Found: (y : A -> B^) | Required: (x: A) -> B^ | + | Note that the existential capture root in B^ + | cannot subsume the capability cap + | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/erased-methods2.check b/tests/neg-custom-args/captures/erased-methods2.check new file mode 100644 index 000000000000..832d9a6c4a10 --- /dev/null +++ b/tests/neg-custom-args/captures/erased-methods2.check @@ -0,0 +1,29 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/erased-methods2.scala:21:9 ------------------------------- +21 | ?=> (x$2: CT[Ex2]^) // error + | ^ + | Found: (erased x$2: CT[Ex2]^) ?->{x$1} Unit + | Required: (erased x$2: CT[Ex2]^) ?->? Unit + | + | Note that the existential capture root in (erased x$2: CT[Ex2]^) ?=> Unit + | cannot subsume the capability x$1.type +22 | ?=> +23 | //given (CT[Ex3]^) = x$1 +24 | Throw(new Ex3) + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/erased-methods2.scala:32:9 ------------------------------- +32 | ?=> (erased x$2: CT[Ex2]^) // error + | ^ + | Found: (erased x$2: CT[Ex2]^) ?->{x$1} (erased x$2: CT[Ex1]^) ?->{x$1} Unit + | Required: (erased x$1²: CT[Ex2]^) ?->? (erased x$2: CT[Ex1]^) ?->? Unit + | + | where: x$1 is a parameter in an anonymous function in method foo10a + | x$1² is a reference to a value parameter + | + | + | Note that the existential capture root in (erased x$1: CT[Ex2]^) ?=> (erased x$2: CT[Ex1]^) ?->{localcap} Unit + | cannot subsume the capability x$1.type +33 | ?=> (erased x$3: CT[Ex1]^) +34 | ?=> Throw(new Ex3) + | + | longer explanation available when compiling with `-explain` diff --git a/tests/pos-custom-args/captures/erased-methods2.scala b/tests/neg-custom-args/captures/erased-methods2.scala similarity index 81% rename from tests/pos-custom-args/captures/erased-methods2.scala rename to tests/neg-custom-args/captures/erased-methods2.scala index 8f1d8897d61a..0b59f741323a 100644 --- a/tests/pos-custom-args/captures/erased-methods2.scala +++ b/tests/neg-custom-args/captures/erased-methods2.scala @@ -16,9 +16,9 @@ def foo8a(i: Int) = def foo9a(i: Int) : (x$1: CT[Ex3]^) ?=> (x$2: CT[Ex2]^) - ?-> {x$1, caps.cap} Unit + ?=> Unit = (x$1: CT[Ex3]^) - ?=> (x$2: CT[Ex2]^) + ?=> (x$2: CT[Ex2]^) // error ?=> //given (CT[Ex3]^) = x$1 Throw(new Ex3) @@ -26,9 +26,9 @@ def foo9a(i: Int) def foo10a(i: Int) : (erased x$0: CT[Ex3]^) ?=> (erased x$1: CT[Ex2]^) - ?-> {x$0, caps.cap} (erased x$2: CT[Ex1]^) - ?-> {x$0, x$1, caps.cap} Unit + ?=> (erased x$2: CT[Ex1]^) + ?=> Unit = (erased x$1: CT[Ex3]^) - ?=> (erased x$2: CT[Ex2]^) + ?=> (erased x$2: CT[Ex2]^) // error ?=> (erased x$3: CT[Ex1]^) ?=> Throw(new Ex3) diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.check b/tests/neg-custom-args/captures/heal-tparam-cs.check new file mode 100644 index 000000000000..6367452db7f7 --- /dev/null +++ b/tests/neg-custom-args/captures/heal-tparam-cs.check @@ -0,0 +1,39 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:15:13 ------------------------------- +15 | localCap { c => // error + | ^ + | Found: (x$0: Capp^) ->? () ->{x$0} Unit + | Required: (c: Capp^) -> () ->{localcap} Unit + | + | Note that the existential capture root in () => Unit + | cannot subsume the capability x$0.type +16 | (c1: Capp^) => () => { c1.use() } +17 | } + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:25:13 ------------------------------- +25 | localCap { c => // error + | ^ + | Found: (x$0: Capp^{io}) ->? () ->{x$0} Unit + | Required: (c: Capp^{io}) -> () ->{net} Unit +26 | (c1: Capp^{io}) => () => { c1.use() } +27 | } + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:41:10 ------------------------------- +41 | io => () => io.use() // error + | ^^^^^^^^^^^^^^ + | Found: () ->{io} Unit + | Required: () ->? Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:44:10 ------------------------------- +44 | io => () => io.use() // error + | ^^^^^^^^^^^^^^ + | Found: () ->{io} Unit + | Required: () ->? Unit + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:10:14 ---------------------------------------------------- +10 | val test1 = localCap { c => // error + | ^^^^^^^^ + | local reference c leaks into outer capture set of type parameter T of method localCap diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.scala b/tests/neg-custom-args/captures/heal-tparam-cs.scala index 4abe014e9b43..6d0b838613f8 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.scala +++ b/tests/neg-custom-args/captures/heal-tparam-cs.scala @@ -12,7 +12,7 @@ def main(io: Capp^, net: Capp^): Unit = { } val test2: (c: Capp^) -> () => Unit = - localCap { c => // ok + localCap { c => // error (c1: Capp^) => () => { c1.use() } } diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index f4fd189ce529..b1e46c300ef7 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -51,6 +51,16 @@ | Required: File^{id*} | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:67:37 -------------------------------------- +67 | val id: (x: File^) -> File^ = x => x // error + | ^ + | Found: (x : File^) + | Required: File^? + | + | Note that the existential capture root in File^ + | cannot subsume the capability x.type + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:71:27 -------------------------------------- 71 | val f1: File^{id*} = id(f) // error // error | ^^^^^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index f8689819f915..32afd4066333 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -64,7 +64,7 @@ def attack2 = f1 def attack3 = - val id: (x: File^) -> File^ = x => x + val id: (x: File^) -> File^ = x => x // error // val id: File^ -> EX C.File^C val leaked = usingFile[File^{id*}]: f => // error From 33ef293621f640b46edc8171f42e49ff498380bb Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 6 Mar 2025 11:41:48 +0100 Subject: [PATCH 73/93] Classify existential variables as root capabilities Allows some simplifications in the zoo of classification methods. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 7 +++++++ .../src/dotty/tools/dotc/cc/CaptureRef.scala | 17 ++++----------- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 21 ++++++------------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 4 ++-- .../src/dotty/tools/dotc/cc/Existential.scala | 9 +------- .../src/dotty/tools/dotc/cc/SepCheck.scala | 4 ++-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 ++-- 7 files changed, 24 insertions(+), 42 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 5aa6fb1f86a0..167f44e1345d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -434,6 +434,13 @@ extension (tp: Type) && tp.membersBasedOnFlags(Mutable | Method, EmptyFlags) .exists(_.hasAltWith(_.symbol.isUpdateMethod)) + /** Knowing that `tp` is a function type, is it an alias to a function other + * than `=>`? + */ + def isAliasFun(using Context) = tp match + case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) + case _ => false + /** Tests whether the type derives from `caps.Capability`, which means * references of this type are maximal capabilities. */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 121fa5a5a979..320955fdac23 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -42,7 +42,7 @@ trait CaptureRef extends TypeProxy, ValueType: * set of the underlying type is not always empty. */ final def isTracked(using Context): Boolean = - this.isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) + this.isTrackableRef && (isRootCapability || !captureSetOfInfo.isAlwaysEmpty) /** Is this a maybe reference of the form `x?`? */ final def isMaybe(using Context): Boolean = this ne stripMaybe @@ -102,25 +102,16 @@ trait CaptureRef extends TypeProxy, ValueType: /** Is this reference one of the generic root capabilities `cap` or `cap.rd` ? */ final def isRootCapability(using Context): Boolean = this match - case ReadOnlyCapability(tp1) => tp1.isCapOrFresh - case _ => isCapOrFresh - - /** Is this reference a capability that does not derive from another capability? - * Includes read-only versions of maximal capabilities. - */ - final def isMaxCapability(using Context): Boolean = this match - case tp: TermRef => tp.isCap || tp.info.derivesFrom(defn.Caps_Exists) + case ReadOnlyCapability(tp1) => tp1.isRootCapability case Existential.Vble(_) => true - case Fresh(_) => true - case ReadOnlyCapability(tp1) => tp1.isMaxCapability - case _ => false + case _ => isCapOrFresh /** An exclusive capability is a capability that derives * indirectly from a maximal capability without going through * a read-only capability first. */ final def isExclusive(using Context): Boolean = - !isReadOnly && (isMaxCapability || captureSetOfInfo.isExclusive) + !isReadOnly && (isRootCapability || captureSetOfInfo.isExclusive) // With the support of paths, we don't need to normalize the `TermRef`s anymore. // /** Normalize reference so that it can be compared with `eq` for equality */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index bc4366192faf..2253ef30baeb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -91,10 +91,6 @@ 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) @@ -102,11 +98,6 @@ sealed abstract class CaptureSet extends Showable: final def containsCap(using Context) = elems.exists(_.stripReadOnly.isCap) - final def isUnboxable(using Context) = - elems.exists: - case Existential.Vble(_) => true - case elem => elem.isRootCapability - final def isReadOnly(using Context): Boolean = elems.forall(_.isReadOnly) @@ -151,7 +142,7 @@ sealed abstract class CaptureSet extends Showable: * capture set. */ protected final def addNewElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = - if elem.isMaxCapability || !vs.isOpen then + if elem.isRootCapability || !vs.isOpen then addThisElem(elem) else addThisElem(elem).orElse: @@ -195,7 +186,7 @@ sealed abstract class CaptureSet extends Showable: elems.exists(_.subsumes(x)) || // Even though subsumes already follows captureSetOfInfo, this is not enough. // For instance x: C^{y, z}. Then neither y nor z subsumes x but {y, z} accounts for x. - !x.isMaxCapability + !x.isRootCapability && !x.derivesFrom(defn.Caps_CapSet) && !(vs.isSeparating && x.captureSetOfInfo.containsRootCapability) // in VarState.Separate, don't try to widen to cap since that might succeed with {cap} <: {cap} @@ -216,7 +207,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, VarState.ClosedUnrecorded)) - || !x.isMaxCapability + || !x.isRootCapability && { val elems = x.captureSetOfInfo.elems !elems.isEmpty && elems.forall(mightAccountFor) @@ -352,7 +343,7 @@ sealed abstract class CaptureSet extends Showable: /** Invoke handler if this set has (or later aquires) the root capability `cap` */ def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = - if isUnboxable then handler() + if containsRootCapability then handler() this /** Invoke handler on the elements to ensure wellformedness of the capture set. @@ -1302,7 +1293,7 @@ object CaptureSet: case ReadOnlyCapability(ref1) => ref1.captureSetOfInfo.map(ReadOnlyMap()) case _ => - if ref.isMaxCapability then ref.singletonCaptureSet + if ref.isRootCapability then ref.singletonCaptureSet else ofType(ref.underlying, followResult = false) /** Capture set of a type @@ -1435,7 +1426,7 @@ object CaptureSet: override def toAdd(using Context) = for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield ccState.levelError = None - if ref.isRootCapability then + if ref.stripReadOnly.isCapOrFresh then def capStr = if ref.isReadOnly then "cap.rd" else "cap" i""" | diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 801800c7ffed..523a0754f1f0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -496,7 +496,7 @@ class CheckCaptures extends Recheck, SymTransformer: val underlying = CaptureSet.ofTypeDeeply(c1.widen) capt.println(i"Widen reach $c to $underlying in ${env.owner}") if ccConfig.useSepChecks then - recur(underlying.filter(!_.isMaxCapability), env, null) + recur(underlying.filter(!_.isRootCapability), env, null) // we don't want to disallow underlying Fresh instances, since these are typically locally created // fresh capabilities. We don't need to also follow the hidden set since separation // checking makes ure that locally hidden references need to go to @consume parameters. @@ -1771,7 +1771,7 @@ class CheckCaptures extends Recheck, SymTransformer: case ref: TermParamRef if !allowed.contains(ref) && !seen.contains(ref) => seen += ref - if ref.isMaxCapability then + if ref.isRootCapability then report.error(i"escaping local reference $ref", tree.srcPos) else val widened = ref.captureSetOfInfo diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 17aa9898da4e..b962a2ac0071 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -250,13 +250,6 @@ object Existential: subst(tp) end toCap - /** Knowing that `tp` is a function type, is it an alias to a function other - * than `=>`? - */ - private def isAliasFun(tp: Type)(using Context) = tp match - case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) - case _ => false - /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound * variable bound by `mt`. * Stop at function or method types since these have been mapped before. @@ -265,7 +258,7 @@ object Existential: abstract class CapMap extends BiTypeMap: override def mapOver(t: Type): Type = t match - case t @ FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => + case t @ FunctionOrMethod(args, res) if variance > 0 && !t.isAliasFun => t // `t` should be mapped in this case by a different call to `mapCap`. case t: (LazyRef | TypeVar) => mapConserveSuper(t) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index f355db76954b..5dc695ba3ed3 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -185,7 +185,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * 3. if `f in F` then the footprint of `f`'s info is also in `F`. */ private def footprint(includeMax: Boolean = false)(using Context): Refs = - def retain(ref: CaptureRef) = includeMax || !ref.isMaxCapability + def retain(ref: CaptureRef) = includeMax || !ref.isRootCapability def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match case newElem :: newElems1 => val superElems = newElem.captureSetOfInfo.elems.filter: superElem => @@ -210,7 +210,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: else hidden.superCaps recur(seen + newElem, acc, superCaps ++ newElems) case _ => - if newElem.isMaxCapability + if newElem.isRootCapability //|| newElem.isInstanceOf[TypeRef | TypeParamRef] then recur(seen + newElem, acc, newElems1) else recur(seen + newElem, acc, newElem.captureSetOfInfo.elems.toList ++ newElems1) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index c5e55f5842c7..5ece12943789 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -926,7 +926,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def apply(t: Type) = t match case t @ CapturingType(parent, refs) => val parent1 = this(parent) - if refs.isUniversalOrFresh then t.derivedCapturingType(parent1, CaptureSet.Fluid) + if refs.containsRootCapability then t.derivedCapturingType(parent1, CaptureSet.Fluid) else t case _ => mapFollowingAliases(t) @@ -982,7 +982,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: for j <- 0 until retained.length if j != i r <- retained(j).toCaptureRefs - if !r.isMaxCapability + if !r.isRootCapability yield r val remaining = CaptureSet(others*) check(remaining, remaining) From 12047d81d4b924dc16832999b6cbe6c5444200ff Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 6 Mar 2025 11:41:48 +0100 Subject: [PATCH 74/93] Classify existential variables as root capabilities Allows some simplifications in the zoo of classification methods. --- .../captures/i22723.scala} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename tests/{pending/pos/classcaps.scala => pos-custom-args/captures/i22723.scala} (59%) diff --git a/tests/pending/pos/classcaps.scala b/tests/pos-custom-args/captures/i22723.scala similarity index 59% rename from tests/pending/pos/classcaps.scala rename to tests/pos-custom-args/captures/i22723.scala index 612a740c8c42..7fc20f813f7c 100644 --- a/tests/pending/pos/classcaps.scala +++ b/tests/pos-custom-args/captures/i22723.scala @@ -1,5 +1,6 @@ import language.experimental.captureChecking +import language.experimental.modularity import caps.* case class A() @@ -10,10 +11,10 @@ trait HasCap: object HasCap: def apply[T](body: HasCap^ ?=> T): T = ??? -class Box(using h: HasCap^): +class Box(using tracked val h: HasCap^): var t: A^{h} = h.mkA def main() = - HasCap: h ?=> - val b = Box(using h) - b.t = h.mkA \ No newline at end of file + HasCap: h1 ?=> + val b = Box(using h1) + b.t = h1.mkA From 399b220a93ff3f1245683c2fc6819764728da699 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 6 Mar 2025 14:42:50 +0100 Subject: [PATCH 75/93] Merge `Existential` and `Fresh` into `root` module --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 9 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 14 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 22 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 28 +- .../src/dotty/tools/dotc/cc/Existential.scala | 123 +------- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 141 +-------- .../src/dotty/tools/dotc/cc/SepCheck.scala | 6 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 14 +- compiler/src/dotty/tools/dotc/cc/root.scala | 283 ++++++++++++++++++ .../tools/dotc/printing/PlainPrinter.scala | 10 +- 10 files changed, 340 insertions(+), 310 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/root.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 167f44e1345d..ee5cc51e6729 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -21,7 +21,7 @@ import CaptureSet.VarState /** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() -/** Context property to print Fresh(...) as "fresh" instead of "cap" */ +/** Context property to print root.Fresh(...) as "fresh" instead of "cap" */ val PrintFresh: Key[Unit] = Key() object ccConfig: @@ -225,7 +225,7 @@ extension (tp: Type) tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet) case tp: TypeParamRef => tp.derivesFrom(defn.Caps_CapSet) - case Existential.Vble(_) => true + case root.Result(_) => true case AnnotatedType(parent, annot) => defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef case _ => @@ -557,7 +557,7 @@ extension (tp: Type) if args.forall(_.isAlwaysPure) then // Also map existentials in results to reach capabilities if all // preceding arguments are known to be always pure - t.derivedFunctionOrMethod(args, apply(Existential.toCap(res))) + t.derivedFunctionOrMethod(args, apply(root.resultToFresh(res))) else t case _ => @@ -720,6 +720,9 @@ extension (tp: AnnotatedType) case ann: CaptureAnnotation => ann.boxed case _ => false + def rootAnnot: root.Annot = (tp.annot: @unchecked) match + case ann: root.Annot => ann + /** Drop retains annotations in the type. */ class CleanupRetains(using Context) extends TypeMap: def apply(tp: Type): Type = diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 320955fdac23..d1f168102b78 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -94,7 +94,7 @@ trait CaptureRef extends TypeProxy, ValueType: /** Is this reference a Fresh instance? */ final def isFresh(using Context): Boolean = this match - case Fresh(_) => true + case root.Fresh(_) => true case _ => false /** Is this reference the generic root capability `cap` or a Fresh instance? */ @@ -103,8 +103,8 @@ trait CaptureRef extends TypeProxy, ValueType: /** Is this reference one of the generic root capabilities `cap` or `cap.rd` ? */ final def isRootCapability(using Context): Boolean = this match case ReadOnlyCapability(tp1) => tp1.isRootCapability - case Existential.Vble(_) => true - case _ => isCapOrFresh + case root(_) => true + case _ => isCap /** An exclusive capability is a capability that derives * indirectly from a maximal capability without going through @@ -244,16 +244,16 @@ trait CaptureRef extends TypeProxy, ValueType: */ def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = def yIsExistential = y.stripReadOnly match - case Existential.Vble(_) => + case root.Result(_) => capt.println(i"failed existential $this >: $y") true case _ => false (this eq y) || this.match - case Fresh(hidden) => + case root.Fresh(hidden) => vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) || !y.stripReadOnly.isCap && !yIsExistential && canAddHidden && vs.addHidden(hidden, y) - case Existential.Vble(binder) => + case root.Result(binder) => if y.derivesFromSharedCapability then true else ccState.existentialSubsumesFailure = @@ -288,7 +288,7 @@ trait CaptureRef extends TypeProxy, ValueType: this match case MaybeCapability(x1) => x1.covers(y1) case _ => false - case Fresh(hidden) => + case root.Fresh(hidden) => hidden.superCaps.exists(this covers _) case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 2253ef30baeb..0e99d6bf8f20 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -410,7 +410,7 @@ object CaptureSet: defn.captureRoot.termRef.singletonCaptureSet def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = - Fresh.withOwner(owner).singletonCaptureSet + root.Fresh.withOwner(owner).singletonCaptureSet /** The shared capture set `{cap.rd}` */ def shared(using Context): CaptureSet = @@ -556,12 +556,12 @@ object CaptureSet: elems -= elem res.addToTrace(this) - // TODO: Also track allowable TermParamRefs and Existential.Vbles in capture sets + // TODO: Also track allowable TermParamRefs and root.Results in capture sets private def levelOK(elem: CaptureRef)(using Context): Boolean = if elem.isRootCapability then !noUniversal else elem match - case elem @ Existential.Vble(mt) => + case elem @ root.Result(mt) => !noUniversal && !CCState.openExistentialScopes.contains(elem) // Opened existentials on the left cannot be added to nested capture sets on the right @@ -619,7 +619,7 @@ object CaptureSet: try val approx = computeApprox(origin).ensuring(_.isConst) if approx.elems.exists: - case Existential.Vble(_) => true + case root.Result(_) => true case _ => false then ccState.approxWarnings += @@ -640,7 +640,7 @@ object CaptureSet: def solve()(using Context): Unit = if !isConst then val approx = upperApprox(empty) - .map(Fresh.FromCap(NoSymbol).inverse) // Fresh --> cap + .map(root.CapToFresh(NoSymbol).inverse) // Fresh --> cap .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems @@ -955,14 +955,14 @@ object CaptureSet: private def aliasRef: AnnotatedType | Null = if myElems.size == 1 then myElems.nth(0) match - case al @ Fresh(hidden) if deps.contains(hidden) => al + case al @ root.Fresh(hidden) if deps.contains(hidden) => al case _ => null else null private def aliasSet: HiddenSet = if myElems.size == 1 then myElems.nth(0) match - case Fresh(hidden) if deps.contains(hidden) => hidden + case root.Fresh(hidden) if deps.contains(hidden) => hidden case _ => this else this @@ -993,7 +993,7 @@ object CaptureSet: assert(dep != this) vs.addHidden(dep.asInstanceOf[HiddenSet], elem) elem match - case Fresh(hidden) => + case root.Fresh(hidden) => if this ne hidden then val alias = hidden.aliasRef if alias != null then @@ -1313,7 +1313,7 @@ object CaptureSet: case tp: (TypeRef | TypeParamRef) => if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet else empty - case tp @ Existential.Vble(_) => + case tp @ root.Result(_) => tp.captureSet case CapturingType(parent, refs) => recur(parent) ++ refs @@ -1331,7 +1331,7 @@ object CaptureSet: ++ recur(rinfo.resType) // add capture set of result .filter: case TermParamRef(binder, _) => binder ne rinfo - case Existential.Vble(binder) => binder ne rinfo + case root.Result(binder) => binder ne rinfo case _ => true case tpd @ AppliedType(tycon, args) => if followResult && defn.isNonRefinedFunction(tpd) then @@ -1377,7 +1377,7 @@ object CaptureSet: if includeTypevars && upper.isExactlyAny then CaptureSet.fresh(t.symbol) else this(cs, upper) case t @ FunctionOrMethod(args, res) => - if args.forall(_.isAlwaysPure) then this(cs, Existential.toCap(res)) + if args.forall(_.isAlwaysPure) then this(cs, root.resultToFresh(res)) else cs case _ => foldOver(cs, t) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 523a0754f1f0..733a8ed4d93b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -325,7 +325,7 @@ class CheckCaptures extends Recheck, SymTransformer: case t @ CapturingType(parent, refs) => for ref <- refs.elems do ref match - case Fresh(hidden) if !hidden.givenOwner.exists => + case root.Fresh(hidden) if !hidden.givenOwner.exists => hidden.givenOwner = sym case _ => traverse(parent) @@ -353,7 +353,7 @@ class CheckCaptures extends Recheck, SymTransformer: /** If `res` is not CompareResult.OK, report an error */ def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = if !res.isOK then - inContext(Fresh.printContext(added, res.blocking)): + inContext(root.printContext(added, res.blocking)): def toAdd: String = CaptureSet.levelErrors.toAdd.mkString def descr: String = val d = res.blocking.description @@ -714,7 +714,7 @@ 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 freshenedFormal = Fresh.fromCap(formal) + val freshenedFormal = root.capToFresh(formal) val argType = recheck(arg, freshenedFormal) .showing(i"recheck arg $arg vs $freshenedFormal", capt) if formal.hasAnnotation(defn.UseAnnot) || formal.hasAnnotation(defn.ConsumeAnnot) then @@ -748,7 +748,7 @@ class CheckCaptures extends Recheck, SymTransformer: */ protected override def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = - val appType = Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes)) + val appType = root.resultToFresh(super.recheckApplication(tree, qualType, funType, argTypes)) val qualCaptures = qualType.captureSet val argCaptures = for (argType, formal) <- argTypes.lazyZip(funType.paramInfos) yield @@ -809,14 +809,14 @@ class CheckCaptures extends Recheck, SymTransformer: * * Second half: union of initial capture set and all capture sets of arguments * to tracked parameters. The initial capture set `initCs` is augmented with - * - Fresh(...) if `core` extends Mutable - * - Fresh(...).rd if `core` extends Capability + * - root.Fresh(...) if `core` extends Mutable + * - root.Fresh(...).rd if `core` extends Capability */ def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = if core.derivesFromMutable then initCs ++ CaptureSet.fresh() - else if core.derivesFromCapability then initCs ++ Fresh.withOwner(core.classSymbol).readOnly.singletonCaptureSet + else if core.derivesFromCapability then initCs ++ root.Fresh.withOwner(core.classSymbol).readOnly.singletonCaptureSet else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol @@ -857,7 +857,7 @@ class CheckCaptures extends Recheck, SymTransformer: case fun @ Select(qual, nme.apply) => qual.symbol.orElse(fun.symbol) case fun => fun.symbol disallowCapInTypeArgs(tree.fun, meth, tree.args) - val res = Existential.toCap(super.recheckTypeApply(tree, pt)) + val res = root.resultToFresh(super.recheckTypeApply(tree, pt)) includeCallCaptures(tree.symbol, res, tree) checkContains(tree) res @@ -909,8 +909,8 @@ class CheckCaptures extends Recheck, SymTransformer: // which are less intelligible. An example is the line `a = x` in // neg-custom-args/captures/vars.scala. That's why this code is conditioned. // to apply only to closures that are not eta expansions. - val res1 = Existential.toCap(res) // TODO: why deep = true? - val pt1 = Existential.toCap(pt) + val res1 = root.resultToFresh(res) // TODO: why deep = true? + val pt1 = root.resultToFresh(pt) // We need to open existentials here in order not to get vars mixed up in them // We do the proper check with existentials when we are finished with the closure block. capt.println(i"pre-check closure $expr of type $res1 against $pt1") @@ -1220,13 +1220,13 @@ class CheckCaptures extends Recheck, SymTransformer: private def existentialSubsumesFailureAddenda(using Context): Addenda = ccState.existentialSubsumesFailure match - case Some((ex @ Existential.Vble(binder), other)) => + case Some((ex @ root.Result(binder), other)) => new Addenda: override def toAdd(using Context): List[String] = - val ann = ex.annot.asInstanceOf[Fresh.Annot] + val ann = ex.rootAnnot i""" | - |Note that the existential capture root in ${ann.originalBinder.resType} + |Note that the existential capture root in ${ex.rootAnnot.originalBinder.resType} |cannot subsume the capability $other""" :: Nil case _ => NothingToAdd @@ -1279,7 +1279,7 @@ class CheckCaptures extends Recheck, SymTransformer: actualBoxed else capt.println(i"conforms failed for ${tree}: $actual vs $expected") - inContext(Fresh.printContext(actualBoxed, expected1)): + inContext(root.printContext(actualBoxed, expected1)): err.typeMismatch(tree.withType(actualBoxed), expected1, addApproxAddenda( addenda diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index b962a2ac0071..c3be58879ccc 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -208,125 +208,4 @@ Expansion of ^: type F = A -> EX c.A -> B^{c} */ -object Existential: - - /** The (super-) type of existentially bound references */ - type Vble = AnnotatedType - - object Vble: - def apply(mt: MethodType)(using Context): Vble = - Fresh.existential(mt) - def unapply(tp: Vble)(using Context): Option[MethodType] = tp.annot match - case ann: Fresh.Annot => - ann.binder match - case mt: MethodType => - assert(ann.hidden.elems.isEmpty) - Some(mt) - case _ => None - case _ => None - - /** Map top-level free existential variables one-to-one to Fresh instances */ - def toCap(tp: Type)(using Context): Type = - val subst = new IdempotentCaptRefMap: - val seen = EqHashMap[Annotation, CaptureRef]() - var localBinders: SimpleIdentitySet[MethodType] = SimpleIdentitySet.empty - - def apply(t: Type): Type = t match - case t @ Vble(binder) => - if localBinders.contains(binder) then t // keep bound references - else seen.getOrElseUpdate(t.annot, Fresh()) // map free references to Fresh() - case t: MethodType => - // skip parameters - val saved = localBinders - if t.marksExistentialScope then localBinders = localBinders + t - try t.derivedLambdaType(resType = this(t.resType)) - finally localBinders = saved - case t: PolyType => - // skip parameters - t.derivedLambdaType(resType = this(t.resType)) - case _ => - mapOver(t) - - subst(tp) - end toCap - - /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound - * variable bound by `mt`. - * Stop at function or method types since these have been mapped before. - */ - def mapCap(tp: Type, mt: MethodType, fail: Message => Unit)(using Context): Type = - - abstract class CapMap extends BiTypeMap: - override def mapOver(t: Type): Type = t match - case t @ FunctionOrMethod(args, res) if variance > 0 && !t.isAliasFun => - t // `t` should be mapped in this case by a different call to `mapCap`. - case t: (LazyRef | TypeVar) => - mapConserveSuper(t) - case _ => - super.mapOver(t) - - object toVar extends CapMap: - private val seen = EqHashMap[CaptureRef, Vble]() - - def apply(t: Type) = t match - case t: CaptureRef if t.isCapOrFresh => - if variance > 0 then - seen.getOrElseUpdate(t, Vble(mt)) - else - if variance == 0 then - fail(em"""$tp captures the root capability `cap` in invariant position. - |This capability cannot be converted to an existential in the result type of a function.""") - // we accept variance < 0, and leave the cap as it is - super.mapOver(t) - case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => - if variance > 0 then - super.mapOver: - defn.FunctionNOf(args, res, contextual) - .capturing(Vble(mt).singletonCaptureSet) - else mapOver(t) - case _ => - mapOver(t) - //.showing(i"mapcap $t = $result") - override def toString = "toVar" - - object inverse extends BiTypeMap: - def apply(t: Type) = t match - case t @ Vble(`mt`) => - // do a reverse getOrElseUpdate on `seen` to produce the - // `Fresh` assosicated with `t` - val it = seen.iterator - var ref: CaptureRef | Null = null - while it.hasNext && ref == null do - val (k, v) = it.next - if v.annot eq t.annot then ref = k - if ref == null then - ref = Fresh() - seen(ref) = t - ref - case _ => mapOver(t) - def inverse = toVar.this - override def toString = "toVar.inverse" - end toVar - - toVar(tp) - end mapCap - - /** Map `cap` in function results to fresh existentials */ - def mapCapInResults(fail: Message => Unit, keepAliases: Boolean = false)(using Context): TypeMap = new TypeMap with FollowAliasesMap: - def apply(t: Type): Type = t match - case defn.RefinedFunctionOf(mt) => - val mt1 = apply(mt) - if mt1 ne mt then mt1.toFunctionType(alwaysDependent = true) - else t - case t: MethodType if variance > 0 && t.marksExistentialScope => - val t1 = mapOver(t).asInstanceOf[MethodType] - t1.derivedLambdaType(resType = mapCap(t1.resType, t1, fail)) - case CapturingType(parent, refs) => - t.derivedCapturingType(this(parent), refs) - case t: (LazyRef | TypeVar) => - mapConserveSuper(t) - case _ => - if keepAliases then mapOver(t) else mapFollowingAliases(t) - end mapCapInResults - -end Existential +object Existential \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 7427a7350e8f..a91222f96cdd 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -19,143 +19,8 @@ import dotty.tools.dotc.util.SimpleIdentitySet /** A module for handling Fresh types. Fresh instances are top types that keep * track of what they hide when capabilities get widened by subsumption to fresh. * The module implements operations to convert between regular caps.cap and - * Fresh instances. Fresh(...) is encoded as `caps.cap @freshCapability(...)` where - * `freshCapability(...)` is a special kind of annotation of type `Fresh.Annot` + * Fresh instances. root.Fresh(...) is encoded as `caps.cap @freshCapability(...)` where + * `freshCapability(...)` is a special kind of annotation of type `root.Annot` * that contains a hidden set. */ -object Fresh: - - /** The annotation of a Fresh instance */ - case class Annot(hidden: CaptureSet.HiddenSet, binder: MethodType | NoType.type) 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 - - private var myOriginalBinder = binder - def originalBinder: MethodType = myOriginalBinder.asInstanceOf[MethodType] - - def derivedAnnotation(binder: MethodType | NoType.type)(using Context): Annotation = - if this.binder eq binder then this - else - val ann = Annot(hidden, binder) - ann.myOriginalBinder = myOriginalBinder - ann - - override def hash: Int = hidden.hashCode - override def eql(that: Annotation) = that match - case Annot(hidden, binder) => (this.hidden eq hidden) && (this.binder eq binder) - case _ => false - - override def mapWith(tm: TypeMap)(using Context) = tm match - case tm: Substituters.SubstBindingMap[MethodType] @unchecked if tm.from eq binder => - derivedAnnotation(tm.to) - case _ => - this - end Annot - - /** Constructor and extractor methods for "fresh" capabilities */ - private def make(owner: Symbol)(using Context): CaptureRef = - if ccConfig.useSepChecks then - val hiddenSet = CaptureSet.HiddenSet(owner) - val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet, NoType)) - hiddenSet.owningCap = res - //assert(hiddenSet.id != 3) - res - else - defn.captureRoot.termRef - - def withOwner(owner: Symbol)(using Context): CaptureRef = make(owner) - - def apply()(using Context): CaptureRef = make(NoSymbol) - - def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match - case Annot(hidden, binder) if !binder.exists => Some(hidden) - case _ => None - - /** Create an existential */ - def existential(binder: MethodType)(using Context): AnnotatedType = - val hiddenSet = CaptureSet.HiddenSet(NoSymbol) - val res = AnnotatedType(defn.captureRoot.termRef, Annot(hiddenSet, binder)) - hiddenSet.owningCap = res - //assert(hiddenSet.id != 9, binder.show) - res - - /** The initial elements (either 0 or 1) of a hidden set created for given `owner`. - * If owner `x` is a trackable this is `x*` if reach` is true, or `x` otherwise. - */ - 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 emptyRefs - else - if ref.isTracked then SimpleIdentitySet(ref) else emptyRefs - - /** Map each occurrence of cap to a different Sep.Cap instance */ - class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: - thisMap => - - override def apply(t: Type) = - if variance <= 0 then t - else t match - case t: CaptureRef if t.isCap => - Fresh.withOwner(owner) - case t @ CapturingType(_, _) => - mapOver(t) - case t @ AnnotatedType(parent, ann) => - val parent1 = this(parent) - if ann.symbol.isRetains && ann.tree.toCaptureSet.containsCap then - this(CapturingType(parent1, ann.tree.toCaptureSet)) - else - t.derivedAnnotatedType(parent1, ann) - case _ => - mapFollowingAliases(t) - - override def toString = "CapToFresh" - - lazy val inverse: BiTypeMap & FollowAliasesMap = new BiTypeMap with FollowAliasesMap: - def apply(t: Type): Type = t match - case t @ Fresh(_) => defn.captureRoot.termRef - case t @ CapturingType(_, refs) => mapOver(t) - case _ => mapFollowingAliases(t) - - def inverse = thisMap - override def toString = thisMap.toString + ".inverse" - - end FromCap - - /** Maps cap to fresh */ - def fromCap(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = - if ccConfig.useSepChecks then FromCap(owner)(tp) else tp - - /** Maps fresh to cap */ - def toCap(tp: Type)(using Context): Type = - if ccConfig.useSepChecks 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` to be printed as `fresh` instead of `cap`, so that one avoids - * confusion in error messages. - */ - def printContext(refs: (Type | CaptureSet)*)(using Context): Context = - def hasCap = new TypeAccumulator[Boolean]: - def apply(x: Boolean, t: Type) = - x || t.dealiasKeepAnnots.match - case Fresh(_) => false - case t: TermRef => t.isCap || this(x, t.widen) - case x: ThisType => false - case _ => foldOver(x, t) - def 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 - - - - - +object Fresh \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 5dc695ba3ed3..f115e5d5e39b 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -202,7 +202,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if seen.contains(newElem) then recur(seen, acc, newElems1) else newElem.stripReadOnly match - case Fresh(hidden) => + case root.Fresh(hidden) => if hidden.deps.isEmpty then recur(seen + newElem, acc + newElem, newElems1) else val superCaps = @@ -271,7 +271,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet def hiddenByElem(elem: CaptureRef): Refs = elem match - case Fresh(hcs) => hcs.elems ++ recur(hcs.elems) + case root.Fresh(hcs) => hcs.elems ++ recur(hcs.elems) case ReadOnlyCapability(ref1) => hiddenByElem(ref1).map(_.readOnly) case _ => emptyRefs @@ -319,7 +319,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def sharedPeaksStr(shared: Refs)(using Context): String = shared.nth(0) match - case fresh @ Fresh(hidden) => + case fresh @ root.Fresh(hidden) => if hidden.owner.exists then i"$fresh of ${hidden.owner}" else i"$fresh" case other => i"$other" diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 5ece12943789..f21f2307f371 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -314,7 +314,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: try val tp1 = mapInferred(refine = true)(tp) - val tp2 = Existential.mapCapInResults(_ => assert(false))(tp1) + val tp2 = root.toResultInResults(_ => assert(false))(tp1) if tp2 ne tp then capt.println(i"expanded inferred in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 catch case ex: AssertionError => @@ -446,7 +446,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def transform(tp: Type): Type = val tp1 = toCapturing(tp) - val tp2 = Existential.mapCapInResults(fail, toCapturing.keepFunAliases)(tp1) + val tp2 = root.toResultInResults(fail, toCapturing.keepFunAliases)(tp1) val snd = if toCapturing.keepFunAliases then "" else " 2nd time" if tp2 ne tp then capt.println(i"expanded explicit$snd in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 @@ -457,7 +457,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: toCapturing.keepFunAliases = false transform(tp1) else tp1 - if freshen then Fresh.fromCap(tp2).tap(addOwnerAsHidden(_, sym)) + if freshen then root.capToFresh(tp2).tap(addOwnerAsHidden(_, sym)) else tp2 end transformExplicitType @@ -514,7 +514,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def add = new TypeTraverser: var reach = false def traverse(t: Type): Unit = t match - case Fresh(hidden) => + case root.Fresh(hidden) => if reach then hidden.elems += ref.reach else if ref.isTracked then hidden.elems += ref case t @ CapturingType(_, _) if t.isBoxed && !reach => @@ -595,7 +595,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: traverse(fn) for case arg: TypeTree <- args do if defn.isTypeTestOrCast(fn.symbol) then - arg.setNuType(Fresh.fromCap(arg.tpe)) + arg.setNuType(root.capToFresh(arg.tpe)) else transformTT(arg, NoSymbol, boxed = true) // type arguments in type applications are boxed @@ -688,7 +688,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: mt.paramInfos else val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) - psyms.map(psym => adaptedInfo(psym, subst(Fresh.toCap(psym.nextInfo)).asInstanceOf[mt.PInfo])), + psyms.map(psym => adaptedInfo(psym, subst(root.freshToCap(psym.nextInfo)).asInstanceOf[mt.PInfo])), mt1 => integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas) ) @@ -702,7 +702,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // If there's a change in the signature, update the info of `sym` if sym.exists && signatureChanges then val newInfo = - Existential.mapCapInResults(report.error(_, tree.srcPos)): + root.toResultInResults(report.error(_, tree.srcPos)): integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil) .showing(i"update info $sym: ${sym.info} = $result", capt) if newInfo ne sym.info then diff --git a/compiler/src/dotty/tools/dotc/cc/root.scala b/compiler/src/dotty/tools/dotc/cc/root.scala new file mode 100644 index 000000000000..a47e44f54616 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/root.scala @@ -0,0 +1,283 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import StdNames.nme +import ast.tpd.* +import Decorators.* +import typer.ErrorReporting.errorType +import Names.TermName +import NameKinds.ExistentialBinderName +import NameOps.isImpureFunction +import CaptureSet.IdempotentCaptRefMap +import reporting.Message +import util.{SimpleIdentitySet, EqHashMap} +import util.Spans.NoSpan +import annotation.internal.sharable + +object root: + + @sharable private var rootId = 0 + + enum Kind: + case Result(binder: MethodType) + case Fresh(hidden: CaptureSet.HiddenSet) + + override def equals(other: Any): Boolean = this match + case Kind.Result(b1) => other match + case Kind.Result(b2) => b1 eq b2 + case _ => false + case Kind.Fresh(h1) => other match + case Kind.Fresh(h2) => h1 eq h2 + case _ => false + end Kind + + /** The annotation of a root instance */ + case class Annot(kind: Kind) extends Annotation: + + /** id printed under -uniqid, for debugging */ + val id = + rootId += 1 + rootId + + override def symbol(using Context) = defn.FreshCapabilityAnnot + override def tree(using Context) = New(symbol.typeRef, Nil) + override def derivedAnnotation(tree: Tree)(using Context): Annotation = this + + private var myOriginalKind = kind + def originalBinder: MethodType = myOriginalKind.asInstanceOf[Kind.Result].binder + + def derivedAnnotation(binder: MethodType)(using Context): Annotation = kind match + case Kind.Result(b) if b ne binder => + val ann = Annot(Kind.Result(binder)) + ann.myOriginalKind = myOriginalKind + ann + case _ => + this + + override def hash: Int = kind.hashCode + override def eql(that: Annotation) = that match + case Annot(kind) => this.kind eq kind + case _ => false + + override def mapWith(tm: TypeMap)(using Context) = kind match + case Kind.Result(binder) => tm match + case tm: Substituters.SubstBindingMap[MethodType] @unchecked if tm.from eq binder => + derivedAnnotation(tm.to) + case _ => this + case _ => this + end Annot + + /** The type of fresh references */ + type Fresh = AnnotatedType + + object Fresh: + /** Constructor and extractor methods for "fresh" capabilities */ + private def make(owner: Symbol)(using Context): CaptureRef = + if ccConfig.useSepChecks then + val hiddenSet = CaptureSet.HiddenSet(owner) + val res = AnnotatedType(defn.captureRoot.termRef, Annot(Kind.Fresh(hiddenSet))) + hiddenSet.owningCap = res + //assert(hiddenSet.id != 3) + res + else + defn.captureRoot.termRef + + def withOwner(owner: Symbol)(using Context): CaptureRef = make(owner) + def apply()(using Context): CaptureRef = make(NoSymbol) + + def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match + case Annot(Kind.Fresh(hidden)) => Some(hidden) + case _ => None + end Fresh + + /** The type of existentially bound references */ + type Result = AnnotatedType + + object Result: + def apply(binder: MethodType)(using Context): Result = + val hiddenSet = CaptureSet.HiddenSet(NoSymbol) + val res = AnnotatedType(defn.captureRoot.termRef, Annot(Kind.Result(binder))) + hiddenSet.owningCap = res + res + + def unapply(tp: Result)(using Context): Option[MethodType] = tp.annot match + case Annot(Kind.Result(binder)) => Some(binder) + case _ => None + end Result + + def unapply(root: AnnotatedType)(using Context): Option[Annot] = + root.annot match + case ann: Annot => Some(ann) + case _ => None + + /** Map each occurrence of cap to a different Sep.Cap instance */ + class CapToFresh(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: + thisMap => + + override def apply(t: Type) = + if variance <= 0 then t + else t match + case t: CaptureRef if t.isCap => + Fresh.withOwner(owner) + case t @ CapturingType(_, _) => + mapOver(t) + case t @ AnnotatedType(parent, ann) => + val parent1 = this(parent) + if ann.symbol.isRetains && ann.tree.toCaptureSet.containsCap then + this(CapturingType(parent1, ann.tree.toCaptureSet)) + else + t.derivedAnnotatedType(parent1, ann) + case _ => + mapFollowingAliases(t) + + override def toString = "CapToFresh" + + lazy val inverse: BiTypeMap & FollowAliasesMap = new BiTypeMap with FollowAliasesMap: + def apply(t: Type): Type = t match + case t @ Fresh(_) => defn.captureRoot.termRef + case t @ CapturingType(_, refs) => mapOver(t) + case _ => mapFollowingAliases(t) + + def inverse = thisMap + override def toString = thisMap.toString + ".inverse" + + end CapToFresh + + /** Maps cap to fresh */ + def capToFresh(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = + if ccConfig.useSepChecks then CapToFresh(owner)(tp) else tp + + /** Maps fresh to cap */ + def freshToCap(tp: Type)(using Context): Type = + if ccConfig.useSepChecks then CapToFresh(NoSymbol).inverse(tp) else tp + + /** Map top-level free existential variables one-to-one to Fresh instances */ + def resultToFresh(tp: Type)(using Context): Type = + val subst = new IdempotentCaptRefMap: + val seen = EqHashMap[Annotation, CaptureRef]() + var localBinders: SimpleIdentitySet[MethodType] = SimpleIdentitySet.empty + + def apply(t: Type): Type = t match + case t @ Result(binder) => + if localBinders.contains(binder) then t // keep bound references + else seen.getOrElseUpdate(t.annot, Fresh()) // map free references to Fresh() + case t: MethodType => + // skip parameters + val saved = localBinders + if t.marksExistentialScope then localBinders = localBinders + t + try t.derivedLambdaType(resType = this(t.resType)) + finally localBinders = saved + case t: PolyType => + // skip parameters + t.derivedLambdaType(resType = this(t.resType)) + case _ => + mapOver(t) + + subst(tp) + end resultToFresh + + /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound + * variable bound by `mt`. + * Stop at function or method types since these have been mapped before. + */ + def toResult(tp: Type, mt: MethodType, fail: Message => Unit)(using Context): Type = + + abstract class CapMap extends BiTypeMap: + override def mapOver(t: Type): Type = t match + case t @ FunctionOrMethod(args, res) if variance > 0 && !t.isAliasFun => + t // `t` should be mapped in this case by a different call to `mapCap`. + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) + case _ => + super.mapOver(t) + + object toVar extends CapMap: + private val seen = EqHashMap[CaptureRef, Result]() + + def apply(t: Type) = t match + case t: CaptureRef if t.isCapOrFresh => + if variance > 0 then + seen.getOrElseUpdate(t, Result(mt)) + else + if variance == 0 then + fail(em"""$tp captures the root capability `cap` in invariant position. + |This capability cannot be converted to an existential in the result type of a function.""") + // we accept variance < 0, and leave the cap as it is + super.mapOver(t) + case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => + if variance > 0 then + super.mapOver: + defn.FunctionNOf(args, res, contextual) + .capturing(Result(mt).singletonCaptureSet) + else mapOver(t) + case _ => + mapOver(t) + //.showing(i"mapcap $t = $result") + override def toString = "toVar" + + object inverse extends BiTypeMap: + def apply(t: Type) = t match + case t @ Result(`mt`) => + // do a reverse getOrElseUpdate on `seen` to produce the + // `Fresh` assosicated with `t` + val it = seen.iterator + var ref: CaptureRef | Null = null + while it.hasNext && ref == null do + val (k, v) = it.next + if v.annot eq t.annot then ref = k + if ref == null then + ref = Fresh() + seen(ref) = t + ref + case _ => mapOver(t) + def inverse = toVar.this + override def toString = "toVar.inverse" + end toVar + + toVar(tp) + end toResult + + /** Map global roots in function results to result roots */ + def toResultInResults(fail: Message => Unit, keepAliases: Boolean = false)(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def apply(t: Type): Type = t match + case defn.RefinedFunctionOf(mt) => + val mt1 = apply(mt) + if mt1 ne mt then mt1.toFunctionType(alwaysDependent = true) + else t + case t: MethodType if variance > 0 && t.marksExistentialScope => + val t1 = mapOver(t).asInstanceOf[MethodType] + t1.derivedLambdaType(resType = toResult(t1.resType, t1, fail)) + case CapturingType(parent, refs) => + t.derivedCapturingType(this(parent), refs) + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) + case _ => + if keepAliases then mapOver(t) else mapFollowingAliases(t) + end toResultInResults + + /** If `refs` contains an occurrence of `cap` or `cap.rd`, the current context + * with an added property PrintFresh. This addition causes all occurrences of + * `Fresh` to be printed as `fresh` instead of `cap`, so that one avoids + * confusion in error messages. + */ + def printContext(refs: (Type | CaptureSet)*)(using Context): Context = + def hasCap = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x || t.dealiasKeepAnnots.match + case Fresh(_) => false + case t: TermRef => t.isCap || this(x, t.widen) + case x: ThisType => false + case _ => foldOver(x, t) + def 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 root \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index bacc357d1474..b8e8bdc660c8 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -261,7 +261,7 @@ class PlainPrinter(_ctx: Context) extends Printer { refs.elems.size == 1 && (refs.isUniversal || !printDebug && !printFresh && !showUniqueIds && refs.elems.nth(0).match - case Existential.Vble(binder) => + case root.Result(binder) => CCState.openExistentialScopes match case b :: _ => binder eq b case _ => false @@ -454,16 +454,16 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" - case tp @ Existential.Vble(binder) => - val idStr = s"##${tp.annot.asInstanceOf[Fresh.Annot].hidden.id}" + case tp @ root.Result(binder) => + val idStr = s"##${tp.rootAnnot.id}" // TODO: Better printing? USe a mode where we print more detailed val vbleText: Text = CCState.openExistentialScopes.indexOf(binder) match case -1 => "" case n => "outer_" * n ++ (if printFresh then "localcap" else "cap") vbleText ~ hashStr(binder) ~ Str(idStr).provided(showUniqueIds) - case Fresh(hidden) => - val idStr = if showUniqueIds then s"#${hidden.id}" else "" + case tp @ root.Fresh(hidden) => + val idStr = if showUniqueIds then s"#${tp.rootAnnot.id}" else "" if printFreshDetailed then s"" else if printFresh then "fresh" else "cap" From 6093dab81e2dc95d093ad033a9a6f22b73078b01 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 6 Mar 2025 17:40:58 +0100 Subject: [PATCH 76/93] Drop Caps_Exists handling and deprecate caps.Exists --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 1 - compiler/src/dotty/tools/dotc/core/Definitions.scala | 1 - library/src/scala/caps.scala | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index f21f2307f371..36843e155037 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -399,7 +399,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def defaultApply(t: Type) = if t.derivesFromCapability && !t.isSingleton - && t.typeSymbol != defn.Caps_Exists && (!sym.isConstructor || (t ne tp.finalResultType)) // Don't add ^ to result types of class constructors deriving from Capability then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3dec2d2f8dda..7b36ddee58f3 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1003,7 +1003,6 @@ class Definitions { @tu lazy val Caps_reachCapability: TermSymbol = CapsModule.requiredMethod("reachCapability") @tu lazy val Caps_readOnlyCapability: TermSymbol = CapsModule.requiredMethod("readOnlyCapability") @tu lazy val Caps_capsOf: TermSymbol = CapsModule.requiredMethod("capsOf") - @tu lazy val Caps_Exists: ClassSymbol = requiredClass("scala.caps.Exists") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @tu lazy val Caps_unsafeAssumeSeparate: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumeSeparate") diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index ca7c7118db43..0ef53595fcc1 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -55,6 +55,7 @@ import annotation.{experimental, compileTimeOnly, retainsCap} * * (x: Exists) => A ->{x} B */ + @deprecated sealed trait Exists extends Capability /** This should go into annotations. For now it is here, so that we From f7484ce55ebf70e49d2cf5a88ff9d8dd733584ba Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Mar 2025 09:46:03 +0100 Subject: [PATCH 77/93] Small tweaks --- compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala | 3 ++- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- compiler/src/dotty/tools/dotc/cc/root.scala | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index f0018cc93d7e..1fab70d647fe 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -42,7 +42,8 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte case cr: TermRef => ref(cr) case cr: TermParamRef => untpd.Ident(cr.paramName).withType(cr) case cr: ThisType => This(cr.cls) - // TODO: Will crash if the type is an annotated type, for example `cap?` + case root(_) => ref(defn.captureRoot.termRef) + // TODO: Will crash if the type is an annotated type, for example `cap.rd` } val arg = repeated(elems, TypeTree(defn.AnyType)) New(symbol.typeRef, arg :: Nil) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index ee5cc51e6729..11bcd6cc5b0e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -437,7 +437,7 @@ extension (tp: Type) /** Knowing that `tp` is a function type, is it an alias to a function other * than `=>`? */ - def isAliasFun(using Context) = tp match + def isAliasFun(using Context): Boolean = tp match case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/root.scala b/compiler/src/dotty/tools/dotc/cc/root.scala index a47e44f54616..9e199607d34e 100644 --- a/compiler/src/dotty/tools/dotc/cc/root.scala +++ b/compiler/src/dotty/tools/dotc/cc/root.scala @@ -25,7 +25,8 @@ object root: case Result(binder: MethodType) case Fresh(hidden: CaptureSet.HiddenSet) - override def equals(other: Any): Boolean = this match + override def equals(other: Any): Boolean = + (this eq other.asInstanceOf[AnyRef]) || this.match case Kind.Result(b1) => other match case Kind.Result(b2) => b1 eq b2 case _ => false @@ -271,13 +272,14 @@ object root: 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 + + def containsCap(x: Type | CaptureSet): Boolean = x match case tp: Type => hasCap(false, tp) case refs: CaptureSet => refs.elems.exists(_.stripReadOnly.isCap) - if refs.exists(containsFresh) then ctx.withProperty(PrintFresh, Some(())) + if refs.exists(containsCap) then ctx.withProperty(PrintFresh, Some(())) else ctx end printContext end root \ No newline at end of file From d27f2bbc8da72adc7576317bd5d375d8b4d903ff Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Mar 2025 18:10:53 +0100 Subject: [PATCH 78/93] Make `cap` not subsume anything by default. Exceptions are bracketed in `withCapAsRoot` calls. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 103 ++++++++++++------ .../src/dotty/tools/dotc/cc/CaptureRef.scala | 18 ++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 49 +++------ .../dotty/tools/dotc/cc/CheckCaptures.scala | 12 +- .../dotty/tools/dotc/typer/RefChecks.scala | 6 +- .../captures/box-adapt-cs.scala | 5 +- .../captures/cc-poly-source.scala | 5 +- .../captures/i19330-alt2.scala | 2 +- tests/neg-custom-args/captures/i19330.check | 7 ++ tests/neg-custom-args/captures/i19330.scala | 2 +- tests/neg-custom-args/captures/i21614.check | 13 ++- .../captures/sep-curried.check | 7 ++ .../captures/sep-curried.scala | 2 +- .../captures/widen-reach.check | 7 ++ .../captures/widen-reach.scala | 2 +- .../captures/cc-poly-source-capability.scala | 4 +- .../captures/reach-capability.scala | 4 +- 17 files changed, 159 insertions(+), 89 deletions(-) rename tests/{pos-custom-args => neg-custom-args}/captures/cc-poly-source.scala (76%) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 11bcd6cc5b0e..5b6c15768025 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -100,6 +100,8 @@ class CCState: private var openExistentialScopes: List[MethodType] = Nil + private var capIsRoot: Boolean = false + object CCState: opaque type Level = Int @@ -144,6 +146,21 @@ object CCState: else op + /** Run `op` under the assumption that `cap` can subsume all other capabilties + * except Result capabilities. Every use of this method should be scrutinized + * for whether it introduces an unsoundness hole. + */ + inline def withCapAsRoot[T](op: => T)(using Context): T = + if isCaptureCheckingOrSetup then + val ccs = ccState + val saved = ccs.capIsRoot + ccs.capIsRoot = true + try op finally ccs.capIsRoot = saved + else op + + /** Is `caps.cap` a root capability that is allowed to subsume other capabilities? */ + def capIsRoot(using Context): Boolean = ccState.capIsRoot + /** The currently opened existential scopes */ def openExistentialScopes(using Context): List[MethodType] = ccState.openExistentialScopes @@ -441,14 +458,30 @@ extension (tp: Type) case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) case _ => false - /** Tests whether the type derives from `caps.Capability`, which means - * references of this type are maximal capabilities. - */ - def derivesFromCapTrait(cls: ClassSymbol)(using Context): Boolean = tp.dealias match + /** Tests whether all CapturingType parts of the type that are traversed for + * dcs computation satisfy at least one of two conditions: + * 1. They decorate classes that extend the given capability class `cls`, or + * 2. Their capture set is constant and consists only of capabilities + * the derive from `cls` in the sense of `derivesFromCapTrait`. + */ + def derivesFromCapTraitDeeply(cls: ClassSymbol)(using Context): Boolean = + val accumulate = new DeepTypeAccumulator[Boolean]: + def capturingCase(acc: Boolean, parent: Type, refs: CaptureSet) = + this(acc, parent) + && (parent.derivesFromCapTrait(cls) + || refs.isConst && refs.elems.forall(_.derivesFromCapTrait(cls))) + def abstractTypeCase(acc: Boolean, t: TypeRef, upperBound: Type) = + this(acc, upperBound) + accumulate(true, tp) + + /** Tests whether the type derives from capability class `cls`. */ + def derivesFromCapTrait(cls: ClassSymbol)(using Context): Boolean = tp.dealiasKeepAnnots match case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol if sym.isClass then sym.derivesFrom(cls) else tp.superType.derivesFromCapTrait(cls) + case ReachCapability(tp1) => + tp1.widen.derivesFromCapTraitDeeply(cls) case tp: (TypeProxy & ValueType) => tp.superType.derivesFromCapTrait(cls) case tp: AndType => @@ -545,7 +578,7 @@ extension (tp: Type) var change = false def apply(t: Type) = if variance <= 0 then t - else t.dealiasKeepAnnots match + else t.dealias match case t @ CapturingType(p, cs) if cs.containsRootCapability => change = true val reachRef = if cs.isReadOnly then ref.reach.readOnly else ref.reach @@ -804,33 +837,6 @@ object ReadOnlyCapability extends AnnotatedCapability(defn.ReadOnlyCapabilityAnn 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: - - /** If `mapped` is a type alias, apply the map to the alias, while keeping - * annotations. If the result is different, return it, otherwise return `mapped`. - * Furthermore, if `original` is a LazyRef or TypeVar and the mapped result is - * the same as the underlying type, keep `original`. This avoids spurious differences - * which would lead to spurious dealiasing in the result - */ - protected def applyToAlias(original: Type, mapped: Type) = - val mapped1 = mapped match - case t: (TypeRef | AppliedType) => - val t1 = t.dealiasKeepAnnots - if t1 eq t then t - else - // If we see a type alias, map the alias type and keep it if it's different - val t2 = apply(t1) - if t2 ne t1 then t2 else t - case _ => - mapped - original match - case original: (LazyRef | TypeVar) if mapped1 eq original.underlying => - original - case _ => - mapped1 -end ConservativeFollowAliasMap - /** An extractor for all kinds of function types as well as method and poly types. * It includes aliases of function types such as `=>`. TODO: Can we do without? * @return 1st half: The argument types or empty if this is a type function @@ -884,3 +890,36 @@ object ContainsParam: if tycon.typeSymbol == defn.Caps_ContainsTrait && cs.typeSymbol.isAbstractOrParamType => Some((cs, ref)) case _ => None + +/** A class encapsulating the assumulator logic needed for `CaptureSet.ofTypeDeeply` + * and `derivesFromCapTraitDeeply`. + * NOTE: The traversal logic needs to be in sync with narrowCaps in CaptureOps, which + * replaces caps with reach capabilties. There are two exceptions, however. + * - First, invariant arguments. These have to be included to be conservative + * in dcs but must be excluded in narrowCaps. + * - Second, unconstrained type variables are handled specially in `ofTypeDeeply`. + */ +abstract class DeepTypeAccumulator[T](using Context) extends TypeAccumulator[T]: + val seen = util.HashSet[Symbol]() + + protected def capturingCase(acc: T, parent: Type, refs: CaptureSet): T + + protected def abstractTypeCase(acc: T, t: TypeRef, upperBound: Type): T + + def apply(acc: T, t: Type) = + if variance < 0 then acc + else t.dealias match + case t @ CapturingType(p, cs1) => + capturingCase(acc, p, cs1) + case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => + seen += t.symbol + abstractTypeCase(acc, t, t.info.bounds.hi) + case AnnotatedType(parent, _) => + this(acc, parent) + case t @ FunctionOrMethod(args, res) => + if args.forall(_.isAlwaysPure) then this(acc, root.resultToFresh(res)) + else acc + case _ => + foldOver(acc, t) +end DeepTypeAccumulator + diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index d1f168102b78..dba10e1fb9f4 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -260,10 +260,20 @@ trait CaptureRef extends TypeProxy, ValueType: ccState.existentialSubsumesFailure.orElse(Some(this, y)) false case _ => - this.isCap && !yIsExistential && canAddHidden && vs != VarState.HardSeparate - || y.match - case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) - case _ => false + y match + case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) + case _ if this.isCap => + y.isCap + || y.derivesFromSharedCapability + || !yIsExistential + && canAddHidden + && vs != VarState.HardSeparate + && (CCState.capIsRoot + // || { println(i"no longer $this maxSubsumes $y, ${y.isCap}"); false } // debug + ) + || false + case _ => + false /** `x covers y` if we should retain `y` when computing the overlap of * two footprints which have `x` respectively `y` as elements. diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 0e99d6bf8f20..4c184b207ea2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -206,7 +206,8 @@ 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, VarState.ClosedUnrecorded)) + CCState.withCapAsRoot: // OK here since we opportunistically choose an alternative which gets checked later + elems.exists(_.subsumes(x)(using ctx, VarState.ClosedUnrecorded)) || !x.isRootCapability && { val elems = x.captureSetOfInfo.elems @@ -639,14 +640,15 @@ object CaptureSet: */ def solve()(using Context): Unit = if !isConst then - val approx = upperApprox(empty) - .map(root.CapToFresh(NoSymbol).inverse) // Fresh --> cap - .showing(i"solve $this = $result", capt) - //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") - val newElems = approx.elems -- elems - given VarState() - if tryInclude(newElems, empty).isOK then - markSolved() + CCState.withCapAsRoot: // // OK here since we infer parameter types that get checked later + val approx = upperApprox(empty) + .map(root.CapToFresh(NoSymbol).inverse) // Fresh --> cap + .showing(i"solve $this = $result", capt) + //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") + val newElems = approx.elems -- elems + given VarState() + if tryInclude(newElems, empty).isOK then + markSolved() /** Mark set as solved and propagate this info to all dependent sets */ def markSolved()(using Context): Unit = @@ -1356,31 +1358,14 @@ object CaptureSet: /** The deep capture set of a type is the union of all covariant occurrences of * capture sets. Nested existential sets are approximated with `cap`. - * NOTE: The traversal logic needs to be in sync with narrowCaps in CaptureOps, which - * replaces caps with reach capabilties. The one exception to this is invariant - * arguments. These have to be included to be conservative in dcs but must be - * excluded in narrowCaps. */ def ofTypeDeeply(tp: Type, includeTypevars: Boolean = false)(using Context): CaptureSet = - val collect = new TypeAccumulator[CaptureSet]: - val seen = util.HashSet[Symbol]() - def apply(cs: CaptureSet, t: Type) = - if variance < 0 then cs - else t.dealias match - case t @ CapturingType(p, cs1) => - this(cs, p) ++ cs1 - case t @ AnnotatedType(parent, ann) => - this(cs, parent) - case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => - seen += t.symbol - val upper = t.info.bounds.hi - if includeTypevars && upper.isExactlyAny then CaptureSet.fresh(t.symbol) - else this(cs, upper) - case t @ FunctionOrMethod(args, res) => - if args.forall(_.isAlwaysPure) then this(cs, root.resultToFresh(res)) - else cs - case _ => - foldOver(cs, t) + val collect = new DeepTypeAccumulator[CaptureSet]: + def capturingCase(acc: CaptureSet, parent: Type, refs: CaptureSet) = + this(acc, parent) ++ refs + def abstractTypeCase(acc: CaptureSet, t: TypeRef, upperBound: Type) = + if includeTypevars && upperBound.isExactlyAny then CaptureSet.fresh(t.symbol) + else this(acc, upperBound) collect(CaptureSet.empty, tp) type AssumedContains = immutable.Map[TypeRef, SimpleIdentitySet[CaptureRef]] diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 733a8ed4d93b..2c0d9a238290 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -698,7 +698,7 @@ 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.stripCapturing.capturing(CaptureSet.universal)) + val argType0 = recheck(arg, pt.stripCapturing.capturing(root.Fresh())) val argType = if argType0.captureSet.isAlwaysEmpty then argType0 else argType0.widen.stripCapturing @@ -1105,7 +1105,8 @@ class CheckCaptures extends Recheck, SymTransformer: for param <- cls.paramGetters do if !param.hasAnnotation(defn.ConstructorOnlyAnnot) && !param.hasAnnotation(defn.UntrackedCapturesAnnot) then - checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) + CCState.withCapAsRoot: // OK? We need this here since self types use `cap` instead of `fresh` + checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) for pureBase <- cls.pureBaseClass do // (4) def selfTypeTree = impl.body .collect: @@ -1452,7 +1453,9 @@ class CheckCaptures extends Recheck, SymTransformer: val cs = actual.captureSet if covariant then cs ++ leaked else - if !leaked.subCaptures(cs).isOK then + if CCState.withCapAsRoot: // Not sure this is OK, actually + !leaked.subCaptures(cs).isOK + then report.error( em"""$expected cannot be box-converted to ${actual.capturing(leaked)} |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", tree.srcPos) @@ -1854,7 +1857,8 @@ class CheckCaptures extends Recheck, SymTransformer: val normArgs = args.lazyZip(tl.paramInfos).map: (arg, bounds) => arg.withType(arg.nuType.forceBoxStatus( bounds.hi.isBoxedCapturing | bounds.lo.isBoxedCapturing)) - checkBounds(normArgs, tl) + CCState.withCapAsRoot: // OK? We need this since bounds use `cap` instead of `fresh` + checkBounds(normArgs, tl) args.lazyZip(tl.paramNames).foreach(healTypeParam(_, _, fun.symbol)) case _ => case _ => diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 6c2a7df06ed0..17e475e50a49 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -21,7 +21,7 @@ import config.MigrationVersion import config.Printers.refcheck import reporting.* import Constants.Constant -import cc.{stripCapturing, isUpdateMethod} +import cc.{stripCapturing, isUpdateMethod, CCState} object RefChecks { import tpd.* @@ -107,7 +107,9 @@ object RefChecks { def checkSelfConforms(other: ClassSymbol) = var otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType) if otherSelf.exists then - if !(cinfo.selfType <:< otherSelf) then + if !CCState.withCapAsRoot: // OK? We need this here since self types use `cap` instead of `fresh` + cinfo.selfType <:< otherSelf + then report.error(DoesNotConformToSelfType("illegal inheritance", cinfo.selfType, cls, otherSelf, "parent", other), cls.srcPos) diff --git a/tests/neg-custom-args/captures/box-adapt-cs.scala b/tests/neg-custom-args/captures/box-adapt-cs.scala index a2a9232fb264..13b81dbd9b5d 100644 --- a/tests/neg-custom-args/captures/box-adapt-cs.scala +++ b/tests/neg-custom-args/captures/box-adapt-cs.scala @@ -5,7 +5,10 @@ def test1(io: Cap^): Unit = { val x: Id[Cap^{io}] = ??? val f: (Cap^) -> Unit = ??? - x(f) // ok + x(f) // error + + val g: (Cap^{io}) -> Unit = ??? + x(g) // ok } def test2(io: Cap^): Unit = { diff --git a/tests/pos-custom-args/captures/cc-poly-source.scala b/tests/neg-custom-args/captures/cc-poly-source.scala similarity index 76% rename from tests/pos-custom-args/captures/cc-poly-source.scala rename to tests/neg-custom-args/captures/cc-poly-source.scala index 2de5c6d67340..e08ea36a6fc9 100644 --- a/tests/pos-custom-args/captures/cc-poly-source.scala +++ b/tests/neg-custom-args/captures/cc-poly-source.scala @@ -27,7 +27,10 @@ import caps.use def test2(@use lbls: List[Label^]) = def makeListener(lbl: Label^): Listener^{lbl} = ??? - val listeners = lbls.map(makeListener) + val listeners = lbls.map(makeListener) // error + // we get an error here because we no longer allow contravariant cap + // to subsume other capabilities. The problem can be solved by declaring + // Label a SharedCapability, see cc-poly-source-capability.scala val src = Source[CapSet^{lbls*}] for l <- listeners do src.register(l) diff --git a/tests/neg-custom-args/captures/i19330-alt2.scala b/tests/neg-custom-args/captures/i19330-alt2.scala index 8c74e05185a2..017ae2c486b3 100644 --- a/tests/neg-custom-args/captures/i19330-alt2.scala +++ b/tests/neg-custom-args/captures/i19330-alt2.scala @@ -11,5 +11,5 @@ trait Foo: def foo: this.T = val leaked = usingLogger[T]: l => // error val t: () => Logger^ = () => l // error separation - t: T + t: T // error leaked diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index ffe6643244af..06a074c40a10 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -3,6 +3,13 @@ | ^^^ | Type variable T of method usingLogger cannot be instantiated to x.T since | the part () => Logger^ of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i19330.scala:17:4 ---------------------------------------- +17 | t: x.T // error + | ^ + | Found: () ->{t} Logger^{t*} + | Required: x.T + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i19330.scala:22:22 --------------------------------------- 22 | val bad: bar.T = foo(bar) // error | ^^^^^^^^ diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index 9c589792a9e3..cdc0367bc7d3 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -14,7 +14,7 @@ class Bar extends Foo: def foo(x: Foo): x.T = val leaked = usingLogger[x.T]: l => // error val t: () => Logger^ = () => l // error - t: x.T + t: x.T // error leaked def test(): Unit = diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index 646e40fb10cb..382768b73bc6 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -1,8 +1,11 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:33 --------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:12 --------------------------------------- 12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? - | ^ - | Found: (f : F) - | Required: File^ + | ^^^^^^^^^^^^^^^^^^^^^^^ + | Found: (f: F) ->{files*.rd} box Logger{val f²: File^?}^? + | Required: (f: box F^{files*.rd}) ->{fresh} box Logger{val f²: File^?}^? + | + | where: f is a reference to a value parameter + | f² is a value in class Logger | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:15:12 --------------------------------------- @@ -11,7 +14,7 @@ | Found: (_$1: box File^{files*}) ->{files*} box Logger{val f: File^{_$1}}^{localcap.rd, _$1} | Required: (_$1: box File^{files*}) ->{fresh} box Logger{val f: File^?}^? | - | Note that reference .rd + | Note that reference .rd | cannot be included in outer capture set ? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/sep-curried.check b/tests/neg-custom-args/captures/sep-curried.check index 313c5c8e461b..edf359c4be14 100644 --- a/tests/neg-custom-args/captures/sep-curried.check +++ b/tests/neg-custom-args/captures/sep-curried.check @@ -1,3 +1,10 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sep-curried.scala:48:43 ---------------------------------- +48 | val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error + | ^^^^^^ + | Found: (y: Ref[Int]^) ->{a} Unit + | Required: (y: Ref[Int]^{a}) ->{a} Unit + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/sep-curried.scala:16:6 -------------------------------------------------------- 16 | foo(a)(a) // error | ^ diff --git a/tests/neg-custom-args/captures/sep-curried.scala b/tests/neg-custom-args/captures/sep-curried.scala index f8abc8a15086..13a07ad6cca7 100644 --- a/tests/neg-custom-args/captures/sep-curried.scala +++ b/tests/neg-custom-args/captures/sep-curried.scala @@ -45,5 +45,5 @@ def test5(): Unit = val a: Ref[Int]^ = Ref(0) val foo: (x: Ref[Int]^) -> (y: Ref[Int]^) ->{x} Unit = x => y => swap(x, y) - val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // should be error, but we don't check params + val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error f(a) diff --git a/tests/neg-custom-args/captures/widen-reach.check b/tests/neg-custom-args/captures/widen-reach.check index 9fe1f2bd5de6..4d4e42601cb0 100644 --- a/tests/neg-custom-args/captures/widen-reach.check +++ b/tests/neg-custom-args/captures/widen-reach.check @@ -13,3 +13,10 @@ | ^^^^^^ | Local reach capability x* leaks into capture scope of method test. | To allow this, the parameter x should be declared with a @use annotation +-- [E164] Declaration Error: tests/neg-custom-args/captures/widen-reach.scala:9:6 -------------------------------------- +9 | val foo: IO^ -> IO^ = x => x // error + | ^ + | error overriding value foo in trait Foo of type IO^ -> box IO^; + | value foo of type IO^ -> IO^ has incompatible type + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/widen-reach.scala b/tests/neg-custom-args/captures/widen-reach.scala index 9a9305640473..6d3a57d6a669 100644 --- a/tests/neg-custom-args/captures/widen-reach.scala +++ b/tests/neg-custom-args/captures/widen-reach.scala @@ -6,7 +6,7 @@ trait Foo[+T]: val foo: IO^ -> T trait Bar extends Foo[IO^]: // error - val foo: IO^ -> IO^ = x => x + val foo: IO^ -> IO^ = x => x // error def test(x: Foo[IO^]): Unit = val y1: Foo[IO^{x*}] = x diff --git a/tests/pos-custom-args/captures/cc-poly-source-capability.scala b/tests/pos-custom-args/captures/cc-poly-source-capability.scala index 6f6bdd91d20a..6f987658923c 100644 --- a/tests/pos-custom-args/captures/cc-poly-source-capability.scala +++ b/tests/pos-custom-args/captures/cc-poly-source-capability.scala @@ -1,11 +1,11 @@ import language.experimental.captureChecking import annotation.experimental -import caps.{CapSet, Capability} +import caps.{CapSet, SharedCapability} import caps.use @experimental object Test: - class Async extends Capability + class Async extends SharedCapability def listener(async: Async): Listener^{async} = ??? diff --git a/tests/pos-custom-args/captures/reach-capability.scala b/tests/pos-custom-args/captures/reach-capability.scala index 08e82a1dabe9..7160b280ce4f 100644 --- a/tests/pos-custom-args/captures/reach-capability.scala +++ b/tests/pos-custom-args/captures/reach-capability.scala @@ -1,6 +1,6 @@ import language.experimental.captureChecking import annotation.experimental -import caps.Capability +import caps.SharedCapability import caps.use @experimental object Test2: @@ -8,7 +8,7 @@ import caps.use class List[+A]: def map[B](f: A => B): List[B] = ??? - class Label extends Capability + class Label extends SharedCapability class Listener From ec8d119679967c373d472df1c779b49c8f6a184c Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Mar 2025 18:23:41 +0100 Subject: [PATCH 79/93] Turn off withCapAsRoot exception for box adaptation TODO: Check that the new error in box-adapt-contra makes sense TODO: error messages need to be improved --- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../captures/box-adapt-contra.check | 15 +++++++++++++++ .../captures/box-adapt-contra.scala | 2 +- tests/neg-custom-args/captures/i15772.check | 16 ++++++---------- 4 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 tests/neg-custom-args/captures/box-adapt-contra.check diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 2c0d9a238290..9841a80b9e8e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1453,7 +1453,7 @@ class CheckCaptures extends Recheck, SymTransformer: val cs = actual.captureSet if covariant then cs ++ leaked else - if CCState.withCapAsRoot: // Not sure this is OK, actually + if // CCState.withCapAsRoot: // Not sure withCapAsRoot is OK here, actually !leaked.subCaptures(cs).isOK then report.error( diff --git a/tests/neg-custom-args/captures/box-adapt-contra.check b/tests/neg-custom-args/captures/box-adapt-contra.check new file mode 100644 index 000000000000..e7ca18e7a9af --- /dev/null +++ b/tests/neg-custom-args/captures/box-adapt-contra.check @@ -0,0 +1,15 @@ +-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:9:52 --------------------------------------------------- +9 | val f: (Cap^{c} -> Unit) -> Unit = useCap[Cap^{c}](c) // error + | ^^^^^^^^^^^^^^^^^^ + | Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{c} Unit + | since the additional capture set {c} resulted from box conversion is not allowed in box Cap^{c} -> Unit +-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:13:57 -------------------------------------------------- +13 | val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // error, was ok when cap was a root + | ^^^^^^^^^^^^^^^^^^^ + | Cap^{c} => Unit cannot be box-converted to box Cap^{c} ->{cap, c} Unit + | since the additional capture set {c} resulted from box conversion is not allowed in box Cap^{c} => Unit +-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:19:54 -------------------------------------------------- +19 | val f3: (Cap^{c} -> Unit) => Unit = useCap3[Cap^{c}](c) // error + | ^^^^^^^^^^^^^^^^^^^ + | Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{d, c} Unit + | since the additional capture set {c} resulted from box conversion is not allowed in box Cap^{c} ->{d} Unit diff --git a/tests/neg-custom-args/captures/box-adapt-contra.scala b/tests/neg-custom-args/captures/box-adapt-contra.scala index 95affbe2aef9..9c18a9a2c50a 100644 --- a/tests/neg-custom-args/captures/box-adapt-contra.scala +++ b/tests/neg-custom-args/captures/box-adapt-contra.scala @@ -10,7 +10,7 @@ def test1(c: Cap^): Unit = def test2(@consume c: Cap^, d: Cap^): Unit = def useCap1[X](x: X): (X => Unit) -> Unit = ??? - val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // ok + val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // error, was ok when cap was a root def useCap2[X](x: X): (X ->{c} Unit) -> Unit = ??? val f2: (Cap^{c} -> Unit) ->{c} Unit = useCap2[Cap^{c}](c) // ok diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index f4d5b4d60189..15eb808972bd 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -3,25 +3,21 @@ | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Int --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:22:46 --------------------------------------- +-- Error: tests/neg-custom-args/captures/i15772.scala:22:46 ------------------------------------------------------------ 22 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error | ^^^^^^^ - | Found: (C{val arg: C^}^{c} => Unit) ->{c} Unit - | Required: (C^ => Unit) -> Unit - | - | longer explanation available when compiling with `-explain` + |C^ => Unit cannot be box-converted to box C{val arg: C^}^{c} ->{cap, c} Unit + |since the additional capture set {c} resulted from box conversion is not allowed in box C{val arg: C^}^{c} => Unit -- Error: tests/neg-custom-args/captures/i15772.scala:28:26 ------------------------------------------------------------ 28 | val c : C^{x} = new C(x) // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Int --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:29:35 --------------------------------------- +-- Error: tests/neg-custom-args/captures/i15772.scala:29:35 ------------------------------------------------------------ 29 | val boxed2 : Observe[C^] = box2(c) // error | ^^^^^^^ - | Found: (C{val arg: C^}^{c} => Unit) ->{c} Unit - | Required: (C^ => Unit) -> Unit - | - | longer explanation available when compiling with `-explain` + |C^ => Unit cannot be box-converted to box C{val arg: C^}^{c} ->{cap, c} Unit + |since the additional capture set {c} resulted from box conversion is not allowed in box C{val arg: C^}^{c} => Unit -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:35:34 --------------------------------------- 35 | val boxed2 : Observe[C]^ = box2(c) // error | ^ From 731c9aaff882cb409a7ef54cc9bde9c7c2ce8b45 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Mar 2025 18:31:43 +0100 Subject: [PATCH 80/93] Test case showing that second soundness hole is filled --- compiler/src/dotty/tools/dotc/cc/root.scala | 4 ++-- tests/neg-custom-args/captures/contracap.check | 7 +++++++ tests/neg-custom-args/captures/contracap.scala | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/neg-custom-args/captures/contracap.check create mode 100644 tests/neg-custom-args/captures/contracap.scala diff --git a/compiler/src/dotty/tools/dotc/cc/root.scala b/compiler/src/dotty/tools/dotc/cc/root.scala index 9e199607d34e..df3bb5f1decd 100644 --- a/compiler/src/dotty/tools/dotc/cc/root.scala +++ b/compiler/src/dotty/tools/dotc/cc/root.scala @@ -19,8 +19,6 @@ import annotation.internal.sharable object root: - @sharable private var rootId = 0 - enum Kind: case Result(binder: MethodType) case Fresh(hidden: CaptureSet.HiddenSet) @@ -35,6 +33,8 @@ object root: case _ => false end Kind + @sharable private var rootId = 0 + /** The annotation of a root instance */ case class Annot(kind: Kind) extends Annotation: diff --git a/tests/neg-custom-args/captures/contracap.check b/tests/neg-custom-args/captures/contracap.check new file mode 100644 index 000000000000..31b58acb80d5 --- /dev/null +++ b/tests/neg-custom-args/captures/contracap.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/contracap.scala:15:48 ------------------------------------ +15 | val g: (Ref[Int]^{a}, Ref[Int]^{a}) -> Unit = f // error + | ^ + | Found: (f : (Ref[Int]^, Ref[Int]^) -> Unit) + | Required: (Ref[Int]^{a}, Ref[Int]^{a}) -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/contracap.scala b/tests/neg-custom-args/captures/contracap.scala new file mode 100644 index 000000000000..0ba1507eb11f --- /dev/null +++ b/tests/neg-custom-args/captures/contracap.scala @@ -0,0 +1,16 @@ +import language.experimental.captureChecking +import caps.* + +class Ref[T](init: T) extends Mutable: + private var value: T = init + def get: T = value + mut def set(newValue: T): Unit = value = newValue + +// a library function that assumes that a and b MUST BE separate +def swap[T](a: Ref[Int]^, b: Ref[Int]^): Unit = ??? + +def test2(): Unit = + val a: Ref[Int]^ = Ref(0) + val f: (Ref[Int]^, Ref[Int]^) -> Unit = swap + val g: (Ref[Int]^{a}, Ref[Int]^{a}) -> Unit = f // error + g(a, a) // OH NO \ No newline at end of file From d89138a39a1483aa1dc1b259f3facc1b0312ef4a Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 8 Mar 2025 19:41:21 +0100 Subject: [PATCH 81/93] Tighten consume checks 1. Strip readonly modifier before checking o=for overlaps 2. Don't reset consume to empty before entering defs --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 ++ .../src/dotty/tools/dotc/cc/SepCheck.scala | 18 ++++-------- .../captures/sep-consume.check | 28 +++++++++++++++++++ .../captures/sep-consume.scala | 26 +++++++++++++++++ .../neg-custom-args/captures/sepchecks5.check | 6 ++++ .../neg-custom-args/captures/sepchecks5.scala | 2 +- 6 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 tests/neg-custom-args/captures/sep-consume.check create mode 100644 tests/neg-custom-args/captures/sep-consume.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 5b6c15768025..a4d113b2ea88 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -482,6 +482,8 @@ extension (tp: Type) else tp.superType.derivesFromCapTrait(cls) case ReachCapability(tp1) => tp1.widen.derivesFromCapTraitDeeply(cls) + case ReadOnlyCapability(tp1) => + tp1.derivesFromCapTrait(cls) case tp: (TypeProxy & ValueType) => tp.superType.derivesFromCapTrait(cls) case tp: AndType => diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index f115e5d5e39b..46d295786959 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -164,13 +164,6 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: /** The set of references that were consumed so far in the current method */ private var consumed: MutConsumedSet = MutConsumedSet() - /** Run `op`` with a fresh, initially empty consumed set. */ - private def withFreshConsumed(op: => Unit): Unit = - val saved = consumed - consumed = MutConsumedSet() - op - consumed = saved - /** Infos aboput Labeled expressions enclosing the current traversal point. * For each labeled expression, it's label name, and a list buffer containing * all consumed sets of return expressions referring to that label. @@ -499,7 +492,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: else assert(!argDeps.isEmpty) if arg.needsSepCheck then - //println(i"testing $arg, ${argPeaks.actual}/${argPeaks.formal} against ${currentPeaks.actual}") + //println(i"testing $arg, formal = ${arg.formalType}, peaks = ${argPeaks.actual}/${argPeaks.hidden} against ${currentPeaks.actual}") checkType(arg.formalType, arg.srcPos, TypeRole.Argument(arg)) // 2. test argPeaks.hidden against previously captured actuals if !argPeaks.hidden.sharedWith(currentPeaks.actual).isEmpty then @@ -548,6 +541,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def checkUse(tree: Tree)(using Context): Unit = val used = tree.markedFree.elems if !used.isEmpty then + capt.println(i"check use $tree: $used") val usedPeaks = used.peaks val overlap = defsShadow.peaks.sharedWith(usedPeaks) if !defsShadow.peaks.sharedWith(usedPeaks).isEmpty then @@ -569,7 +563,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: sepUseError(tree, null, used, defsShadow) for ref <- used do - val pos = consumed.get(ref) + val pos = consumed.get(ref.stripReadOnly) if pos != null then consumeError(ref, pos, tree.srcPos) end checkUse @@ -656,7 +650,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case _: TypeRole.Argument | _: TypeRole.Qualifier => for ref <- refsToCheck do if !ref.derivesFromSharedCapability then - consumed.put(ref, pos) + consumed.put(ref.stripReadOnly, pos) case _ => end checkConsumedRefs @@ -913,7 +907,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: checkValOrDefDef(tree) case tree: DefDef => inSection: - withFreshConsumed: + consumed.segment: for params <- tree.paramss; case param: ValDef <- params do pushDef(param, emptyRefs) traverseChildren(tree) @@ -945,7 +939,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val caseConsumed = for cas <- cases yield consumed.segment(traverse(cas)) caseConsumed.foreach(consumed ++= _) case tree: TypeDef if tree.symbol.isClass => - withFreshConsumed: + consumed.segment: traverseChildren(tree) case tree: WhileDo => val loopConsumed = consumed.segment(traverseChildren(tree)) diff --git a/tests/neg-custom-args/captures/sep-consume.check b/tests/neg-custom-args/captures/sep-consume.check new file mode 100644 index 000000000000..69229efe07b7 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-consume.check @@ -0,0 +1,28 @@ +-- Error: tests/neg-custom-args/captures/sep-consume.scala:17:2 -------------------------------------------------------- +17 | x.put(42) // error + | ^ + | Separation failure: Illegal access to (x : Ref^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 16 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/sep-consume.scala:18:2 -------------------------------------------------------- +18 | x.get // error + | ^ + | Separation failure: Illegal access to x.rd, which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 16 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/sep-consume.scala:19:16 ------------------------------------------------------- +19 | par(rx, () => x.put(42)) // error + | ^ + | Separation failure: Illegal access to (x : Ref^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 16 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/sep-consume.scala:20:16 ------------------------------------------------------- +20 | par(rx, () => x.get) // error + | ^ + | Separation failure: Illegal access to x.rd, which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 16 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/sep-consume.scala:24:16 ------------------------------------------------------- +24 | def foo = bad(f) // error + | ^ + | Separation failure: argument to @consume parameter with type (f : () ->{x.rd} Unit) refers to non-local value f diff --git a/tests/neg-custom-args/captures/sep-consume.scala b/tests/neg-custom-args/captures/sep-consume.scala new file mode 100644 index 000000000000..6c3b4806ba8e --- /dev/null +++ b/tests/neg-custom-args/captures/sep-consume.scala @@ -0,0 +1,26 @@ +import language.experimental.captureChecking +import caps.* + +class Ref extends Mutable: + private var _data = 0 + def get: Int = _data + mut def put(x: Int): Unit = _data = x + +// require f and g to be non-interfering +def par(f: () => Unit, g: () => Unit): Unit = ??? + +def bad(@consume op: () ->{cap.rd} Unit): () => Unit = op + +def test2(@consume x: Ref^): Unit = + val f: () ->{x.rd} Unit = () => x.get + val rx: () => Unit = bad(f) // hides x.rd in the resulting `cap` + x.put(42) // error + x.get // error + par(rx, () => x.put(42)) // error + par(rx, () => x.get) // error + +def test3(@consume x: Ref^): Unit = + val f: () ->{x.rd} Unit = () => x.get + def foo = bad(f) // error + foo() + foo() diff --git a/tests/neg-custom-args/captures/sepchecks5.check b/tests/neg-custom-args/captures/sepchecks5.check index b65b6a46e6e2..aae5cb89da53 100644 --- a/tests/neg-custom-args/captures/sepchecks5.check +++ b/tests/neg-custom-args/captures/sepchecks5.check @@ -8,3 +8,9 @@ | ^^ | Separation failure: argument to @consume parameter with type (io : Object^) refers to parameter io. | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks5.scala:20:24 -------------------------------------------------------- +20 | par(f2)(() => println(io)) // error + | ^^ + | Separation failure: Illegal access to (io : Object^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 19 + | and therefore is no longer available. diff --git a/tests/neg-custom-args/captures/sepchecks5.scala b/tests/neg-custom-args/captures/sepchecks5.scala index 5e2d4796f9f7..9a16051e64bd 100644 --- a/tests/neg-custom-args/captures/sepchecks5.scala +++ b/tests/neg-custom-args/captures/sepchecks5.scala @@ -17,5 +17,5 @@ def test(io: Object^): Unit = par(f1)(() => println(io)) // !!! separation failure val f2 = g(io) // error - par(f2)(() => println(io)) // !!! separation failure + par(f2)(() => println(io)) // error From a6f1ab2f235cdcbc1858550e43fdc4032f411a00 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 9 Mar 2025 12:53:03 +0100 Subject: [PATCH 82/93] Split posCC from pos tests I believe the timeout was simply that we have to many pos tests, so theior compilation takes more than the limit of 20 minutes. Testing this hypothesis by moving pos-custumargs/captures tests into a separate top-level test posCC --- compiler/test/dotty/tools/dotc/CompilationTests.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index e62c80d7bff7..47ed2aa6564d 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -37,7 +37,6 @@ class CompilationTests { compileFilesInDir("tests/pos-special/sourcepath/outer", defaultOptions.and("-sourcepath", "tests/pos-special/sourcepath")), compileFile("tests/pos-special/sourcepath/outer/nested/Test4.scala", defaultOptions.and("-sourcepath", "tests/pos-special/sourcepath")), compileFilesInDir("tests/pos-scala2", defaultOptions.and("-source", "3.0-migration")), - compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")), compileFile("tests/pos-special/utf8encoded.scala", defaultOptions.and("-encoding", "UTF8")), compileFile("tests/pos-special/utf16encoded.scala", defaultOptions.and("-encoding", "UTF16")), compileDir("tests/pos-special/i18589", defaultOptions.and("-Wsafe-init").without("-Ycheck:all")), @@ -56,6 +55,12 @@ class CompilationTests { aggregateTests(tests*).checkCompile() } + @Test def posCC: Unit = + given TestGroup = TestGroup("compilePosCC") + aggregateTests( + compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")), + ).checkCompile() + @Test def rewrites: Unit = { implicit val testGroup: TestGroup = TestGroup("rewrites") From d2e276c331e7989417c605f937674136071990f8 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 9 Mar 2025 19:43:46 +0100 Subject: [PATCH 83/93] Fine-tune pruning for consume checks of arguments Deduct explicit refs of actual as opposed to formal argument type. --- .../src/dotty/tools/dotc/cc/SepCheck.scala | 27 ++++++++------- .../captures/sep-consume.check | 34 +++++++++++-------- .../captures/sep-consume.scala | 15 ++++++++ 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 46d295786959..cc688fc7b8c8 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -567,24 +567,16 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: if pos != null then consumeError(ref, pos, tree.srcPos) end checkUse - /** If `tp` denotes some version of a singleton type `x.type` the set `{x}` + /** If `tp` denotes some version of a singleton capture ref `x.type` the set `{x, x*}` * otherwise the empty set. */ - def explicitRefs(tp: Type): Refs = tp match - case tp: (TermRef | ThisType) => SimpleIdentitySet(tp) + def explicitRefs(tp: Type)(using Context): Refs = tp match + case tp: (TermRef | ThisType) if tp.isTrackableRef => SimpleIdentitySet(tp, tp.reach) case AnnotatedType(parent, _) => explicitRefs(parent) case AndType(tp1, tp2) => explicitRefs(tp1) ++ explicitRefs(tp2) case OrType(tp1, tp2) => explicitRefs(tp1) ** explicitRefs(tp2) case _ => emptyRefs - /** Deduct some elements from `refs` according to the role of the checked type `tpe`: - * - If the the type apears as a (result-) type of a definition of `x`, deduct - * `x` and `x*`. - * - If `tpe` is morally a singleton type deduct it as well. - */ - def prune(refs: Refs, tpe: Type, role: TypeRole)(using Context): Refs = - refs.deductSymFootprint(role.dclSym).deduct(explicitRefs(tpe)) - /** Check validity of consumed references `refsToCheck`. The references are consumed * because they are hidden in a Fresh result type or they are referred * to in an argument to a @consume parameter or in a prefix of a @consume method -- @@ -611,7 +603,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def checkConsumedRefs(refsToCheck: Refs, tpe: Type, role: TypeRole, descr: => String, pos: SrcPos)(using Context) = val badParams = mutable.ListBuffer[Symbol]() def currentOwner = role.dclSym.orElse(ctx.owner) - for hiddenRef <- prune(refsToCheck, tpe, role) do + for hiddenRef <- refsToCheck.deductSymRefs(role.dclSym).deduct(explicitRefs(tpe)) do if !hiddenRef.derivesFromSharedCapability then hiddenRef.pathRoot match case ref: TermRef => @@ -662,8 +654,17 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: */ def checkType(tpe: Type, pos: SrcPos, role: TypeRole)(using Context): Unit = + /** Deduct some elements from `refs` according to the role of the checked type `tpe`: + * - If the the type apears as a (result-) type of a definition of `x`, deduct + * `x` and `x*`. + * - If the checked type (or, for arguments, the actual type of the argument) + * is morally a singleton type `y.type` deduct `y` and `y*` as well. + */ extension (refs: Refs) def pruned = - refs.deductSymRefs(role.dclSym).deduct(explicitRefs(tpe)) + val deductedType = role match + case TypeRole.Argument(arg) => arg.tpe + case _ => tpe + refs.deductSymRefs(role.dclSym).deduct(explicitRefs(deductedType)) def sepTypeError(parts: List[Type], genPart: Type, otherPart: Type): Unit = val captured = genPart.deepCaptureSet.elems diff --git a/tests/neg-custom-args/captures/sep-consume.check b/tests/neg-custom-args/captures/sep-consume.check index 69229efe07b7..e26720048efb 100644 --- a/tests/neg-custom-args/captures/sep-consume.check +++ b/tests/neg-custom-args/captures/sep-consume.check @@ -1,28 +1,34 @@ --- Error: tests/neg-custom-args/captures/sep-consume.scala:17:2 -------------------------------------------------------- -17 | x.put(42) // error +-- Error: tests/neg-custom-args/captures/sep-consume.scala:19:2 -------------------------------------------------------- +19 | x.put(42) // error | ^ | Separation failure: Illegal access to (x : Ref^), which was passed to a - | @consume parameter or was used as a prefix to a @consume method on line 16 + | @consume parameter or was used as a prefix to a @consume method on line 18 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/sep-consume.scala:18:2 -------------------------------------------------------- -18 | x.get // error +-- Error: tests/neg-custom-args/captures/sep-consume.scala:20:2 -------------------------------------------------------- +20 | x.get // error | ^ | Separation failure: Illegal access to x.rd, which was passed to a - | @consume parameter or was used as a prefix to a @consume method on line 16 + | @consume parameter or was used as a prefix to a @consume method on line 18 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/sep-consume.scala:19:16 ------------------------------------------------------- -19 | par(rx, () => x.put(42)) // error +-- Error: tests/neg-custom-args/captures/sep-consume.scala:21:16 ------------------------------------------------------- +21 | par(rx, () => x.put(42)) // error | ^ | Separation failure: Illegal access to (x : Ref^), which was passed to a - | @consume parameter or was used as a prefix to a @consume method on line 16 + | @consume parameter or was used as a prefix to a @consume method on line 18 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/sep-consume.scala:20:16 ------------------------------------------------------- -20 | par(rx, () => x.get) // error +-- Error: tests/neg-custom-args/captures/sep-consume.scala:22:16 ------------------------------------------------------- +22 | par(rx, () => x.get) // error | ^ | Separation failure: Illegal access to x.rd, which was passed to a - | @consume parameter or was used as a prefix to a @consume method on line 16 + | @consume parameter or was used as a prefix to a @consume method on line 18 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/sep-consume.scala:24:16 ------------------------------------------------------- -24 | def foo = bad(f) // error +-- Error: tests/neg-custom-args/captures/sep-consume.scala:26:16 ------------------------------------------------------- +26 | def foo = bad(f) // error | ^ | Separation failure: argument to @consume parameter with type (f : () ->{x.rd} Unit) refers to non-local value f +-- Error: tests/neg-custom-args/captures/sep-consume.scala:34:12 ------------------------------------------------------- +34 | println(p.fst.get) // errorSep + | ^^^^^ + | Separation failure: Illegal access to p.fst*, which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 33 + | and therefore is no longer available. diff --git a/tests/neg-custom-args/captures/sep-consume.scala b/tests/neg-custom-args/captures/sep-consume.scala index 6c3b4806ba8e..6ad76b26f736 100644 --- a/tests/neg-custom-args/captures/sep-consume.scala +++ b/tests/neg-custom-args/captures/sep-consume.scala @@ -6,6 +6,8 @@ class Ref extends Mutable: def get: Int = _data mut def put(x: Int): Unit = _data = x +case class Pair[+A, +B](fst: A, snd: B) + // require f and g to be non-interfering def par(f: () => Unit, g: () => Unit): Unit = ??? @@ -24,3 +26,16 @@ def test3(@consume x: Ref^): Unit = def foo = bad(f) // error foo() foo() + +def test4(@consume @use p: Pair[Ref^, Ref^]): Unit = + val x: Ref^{p.fst*} = p.fst + val y: Ref^{p.snd*} = p.snd + badp(Pair(x, y)) + println(p.fst.get) // errorSep + +def badp(@consume p: Pair[Ref^, Ref^]): Unit = () + +def test5(@consume @use p: Pair[Ref^, Ref^]): Unit = + badp(p) // ok + println(p.fst.get) // ok, but should be error + From f54758227f9d94fbc545ab1babf3d57a2fe7a0cb Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 9 Mar 2025 19:54:52 +0100 Subject: [PATCH 84/93] Refactor Move extension methods to SepChecks object, drop unused ones. --- .../src/dotty/tools/dotc/cc/SepCheck.scala | 56 ++++++++----------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index cc688fc7b8c8..41f96d748002 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -147,29 +147,6 @@ object SepCheck: case class DefInfo(tree: ValOrDefDef, symbol: Symbol, hidden: Refs, hiddenPeaks: Refs) -class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: - import checker.* - import SepCheck.* - - /** The set of capabilities that are hidden by a polymorphic result type - * of some previous definition. - */ - private var defsShadow: Refs = emptyRefs - - /** The previous val or def definitions encountered during separation checking - * in reverse order. These all enclose and precede the current traversal node. - */ - private var previousDefs: List[DefInfo] = Nil - - /** The set of references that were consumed so far in the current method */ - private var consumed: MutConsumedSet = MutConsumedSet() - - /** Infos aboput Labeled expressions enclosing the current traversal point. - * For each labeled expression, it's label name, and a list buffer containing - * all consumed sets of return expressions referring to that label. - */ - private var openLabeled: List[(Name, mutable.ListBuffer[ConsumedSet])] = Nil - extension (refs: Refs) /** The footprint of a set of references `refs` the smallest set `F` such that @@ -280,24 +257,37 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: refs.filter: ref => !others.exists(_.covers(ref)) - /** Deduct the footprint of `sym` and `sym*` from `refs` */ - private def deductSymFootprint(sym: Symbol)(using Context): Refs = - val ref = sym.termRef - if ref.isTrackableRef then refs.deduct(CaptureSet(ref, ref.reach).elems.footprint()) - else refs - /** Deduct `sym` and `sym*` from `refs` */ private def deductSymRefs(sym: Symbol)(using Context): Refs = val ref = sym.termRef if ref.isTrackableRef then refs.deduct(SimpleIdentitySet(ref, ref.reach)) else refs - /** Deduct the footprint of all captures of trees in `deps` from `refs` */ - private def deductCapturesOf(deps: List[Tree])(using Context): Refs = - deps.foldLeft(refs): (refs, dep) => - refs.deduct(captures(dep).footprint()) end extension +class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: + import checker.* + import SepCheck.* + + /** The set of capabilities that are hidden by a polymorphic result type + * of some previous definition. + */ + private var defsShadow: Refs = emptyRefs + + /** The previous val or def definitions encountered during separation checking + * in reverse order. These all enclose and precede the current traversal node. + */ + private var previousDefs: List[DefInfo] = Nil + + /** The set of references that were consumed so far in the current method */ + private var consumed: MutConsumedSet = MutConsumedSet() + + /** Infos aboput Labeled expressions enclosing the current traversal point. + * For each labeled expression, it's label name, and a list buffer containing + * all consumed sets of return expressions referring to that label. + */ + private var openLabeled: List[(Name, mutable.ListBuffer[ConsumedSet])] = Nil + /** The deep capture set of an argument or prefix widened to the formal parameter, if * the latter contains a cap. */ From ee60b210019c504ac2fa6f6e14651d5d6a462491 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 9 Mar 2025 20:34:48 +0100 Subject: [PATCH 85/93] Fix compute overlap logic for consumes - Make it peak-based instead of footprint-based - Don't flag rd/rd conflicts between consumed and re-used --- .../src/dotty/tools/dotc/cc/SepCheck.scala | 25 ++++++++++++++++--- .../captures/sep-consume.check | 20 ++++++--------- .../captures/sep-consume.scala | 8 +++--- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 41f96d748002..c8ab2ccbe81a 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -97,6 +97,7 @@ object SepCheck: var refs: Array[CaptureRef] = new Array(4) var locs: Array[SrcPos] = new Array(4) var size = 0 + var peaks: Refs = emptyRefs private def double[T <: AnyRef : ClassTag](xs: Array[T]): Array[T] = val xs1 = new Array[T](xs.length * 2) @@ -114,18 +115,29 @@ object SepCheck: while i < size && (refs(i) ne ref) do i += 1 if i < size then locs(i) else null + def clashing(ref: CaptureRef)(using Context): SrcPos | Null = + val refPeaks = ref.peaks + if !peaks.sharedWith(refPeaks).isEmpty then + var i = 0 + while i < size && refs(i).peaks.sharedWith(refPeaks).isEmpty do + i += 1 + assert(i < size) + locs(i) + else null + /** If `ref` is not yet in the set, add it with given source position */ - def put(ref: CaptureRef, loc: SrcPos): Unit = + def put(ref: CaptureRef, loc: SrcPos)(using Context): Unit = if get(ref) == null then ensureCapacity(1) refs(size) = ref locs(size) = loc size += 1 + peaks = peaks ++ ref.peaks /** Add all references with their associated positions from `that` which * are not yet in the set. */ - def ++= (that: ConsumedSet): Unit = + def ++= (that: ConsumedSet)(using Context): Unit = for i <- 0 until that.size do put(that.refs(i), that.locs(i)) /** Run `op` and return any new references it created in a separate `ConsumedSet`. @@ -133,12 +145,14 @@ object SepCheck: */ def segment(op: => Unit): ConsumedSet = val start = size + val savedPeaks = peaks try op if size == start then EmptyConsumedSet else ConstConsumedSet(refs.slice(start, size), locs.slice(start, size)) finally size = start + peaks = savedPeaks end MutConsumedSet val EmptyConsumedSet = ConstConsumedSet(Array(), Array()) @@ -265,6 +279,9 @@ object SepCheck: end extension + extension (ref: CaptureRef) + def peaks(using Context): Refs = SimpleIdentitySet(ref).peaks + class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import checker.* import SepCheck.* @@ -553,7 +570,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: sepUseError(tree, null, used, defsShadow) for ref <- used do - val pos = consumed.get(ref.stripReadOnly) + val pos = consumed.clashing(ref) if pos != null then consumeError(ref, pos, tree.srcPos) end checkUse @@ -632,7 +649,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case _: TypeRole.Argument | _: TypeRole.Qualifier => for ref <- refsToCheck do if !ref.derivesFromSharedCapability then - consumed.put(ref.stripReadOnly, pos) + consumed.put(ref, pos) case _ => end checkConsumedRefs diff --git a/tests/neg-custom-args/captures/sep-consume.check b/tests/neg-custom-args/captures/sep-consume.check index e26720048efb..d8ee2c5ed43e 100644 --- a/tests/neg-custom-args/captures/sep-consume.check +++ b/tests/neg-custom-args/captures/sep-consume.check @@ -4,31 +4,25 @@ | Separation failure: Illegal access to (x : Ref^), which was passed to a | @consume parameter or was used as a prefix to a @consume method on line 18 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/sep-consume.scala:20:2 -------------------------------------------------------- -20 | x.get // error - | ^ - | Separation failure: Illegal access to x.rd, which was passed to a - | @consume parameter or was used as a prefix to a @consume method on line 18 - | and therefore is no longer available. -- Error: tests/neg-custom-args/captures/sep-consume.scala:21:16 ------------------------------------------------------- 21 | par(rx, () => x.put(42)) // error | ^ | Separation failure: Illegal access to (x : Ref^), which was passed to a | @consume parameter or was used as a prefix to a @consume method on line 18 | and therefore is no longer available. --- Error: tests/neg-custom-args/captures/sep-consume.scala:22:16 ------------------------------------------------------- -22 | par(rx, () => x.get) // error - | ^ - | Separation failure: Illegal access to x.rd, which was passed to a - | @consume parameter or was used as a prefix to a @consume method on line 18 - | and therefore is no longer available. -- Error: tests/neg-custom-args/captures/sep-consume.scala:26:16 ------------------------------------------------------- 26 | def foo = bad(f) // error | ^ | Separation failure: argument to @consume parameter with type (f : () ->{x.rd} Unit) refers to non-local value f -- Error: tests/neg-custom-args/captures/sep-consume.scala:34:12 ------------------------------------------------------- -34 | println(p.fst.get) // errorSep +34 | println(p.fst.get) // error | ^^^^^ | Separation failure: Illegal access to p.fst*, which was passed to a | @consume parameter or was used as a prefix to a @consume method on line 33 | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/sep-consume.scala:40:12 ------------------------------------------------------- +40 | println(p.fst.get) // error + | ^^^^^ + | Separation failure: Illegal access to p.fst*, which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 39 + | and therefore is no longer available. diff --git a/tests/neg-custom-args/captures/sep-consume.scala b/tests/neg-custom-args/captures/sep-consume.scala index 6ad76b26f736..46c45e8cc6f5 100644 --- a/tests/neg-custom-args/captures/sep-consume.scala +++ b/tests/neg-custom-args/captures/sep-consume.scala @@ -17,9 +17,9 @@ def test2(@consume x: Ref^): Unit = val f: () ->{x.rd} Unit = () => x.get val rx: () => Unit = bad(f) // hides x.rd in the resulting `cap` x.put(42) // error - x.get // error + x.get // ok rd/rd par(rx, () => x.put(42)) // error - par(rx, () => x.get) // error + par(rx, () => x.get) // ok rd/rd def test3(@consume x: Ref^): Unit = val f: () ->{x.rd} Unit = () => x.get @@ -31,11 +31,11 @@ def test4(@consume @use p: Pair[Ref^, Ref^]): Unit = val x: Ref^{p.fst*} = p.fst val y: Ref^{p.snd*} = p.snd badp(Pair(x, y)) - println(p.fst.get) // errorSep + println(p.fst.get) // error def badp(@consume p: Pair[Ref^, Ref^]): Unit = () def test5(@consume @use p: Pair[Ref^, Ref^]): Unit = badp(p) // ok - println(p.fst.get) // ok, but should be error + println(p.fst.get) // error From c790007d4492aed4b64d048cbf7332551940a9c5 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 10 Mar 2025 23:06:10 +0100 Subject: [PATCH 86/93] Remove unused files and document root.scala --- .../tools/dotc/cc/CaptureAnnotation.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 10 +- .../src/dotty/tools/dotc/cc/Existential.scala | 211 ------------------ compiler/src/dotty/tools/dotc/cc/Fresh.scala | 26 --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 +- .../src/dotty/tools/dotc/cc/Synthetics.scala | 4 +- compiler/src/dotty/tools/dotc/cc/root.scala | 66 +++++- .../dotty/tools/dotc/core/Definitions.scala | 7 +- .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../annotation/internal/freshCapability.scala | 10 - library/src/scala/caps.scala | 25 +-- 12 files changed, 84 insertions(+), 285 deletions(-) delete mode 100644 compiler/src/dotty/tools/dotc/cc/Existential.scala delete mode 100644 compiler/src/dotty/tools/dotc/cc/Fresh.scala delete mode 100644 library/src/scala/annotation/internal/freshCapability.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index 1fab70d647fe..2be492ed6189 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -42,7 +42,7 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte case cr: TermRef => ref(cr) case cr: TermParamRef => untpd.Ident(cr.paramName).withType(cr) case cr: ThisType => This(cr.cls) - case root(_) => ref(defn.captureRoot.termRef) + case root(_) => ref(root.cap) // TODO: Will crash if the type is an annotated type, for example `cap.rd` } val arg = repeated(elems, TypeTree(defn.AnyType)) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a4d113b2ea88..57c91704bd7d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -218,7 +218,7 @@ extension (tree: Tree) elems case _ => if tree.symbol.maybeOwner == defn.RetainsCapAnnot - then ref(defn.captureRoot.termRef) :: Nil + then ref(root.cap) :: Nil else Nil extension (tp: Type) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 4c184b207ea2..05dff8e05875 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -408,14 +408,20 @@ object CaptureSet: /** The universal capture set `{cap}` */ def universal(using Context): CaptureSet = - defn.captureRoot.termRef.singletonCaptureSet + root.cap.singletonCaptureSet + + /** The same as CaptureSet.universal but generated implicitly for + * references of Capability subtypes + */ + def universalImpliedByCapability(using Context) = + defn.universalCSImpliedByCapability def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = root.Fresh.withOwner(owner).singletonCaptureSet /** The shared capture set `{cap.rd}` */ def shared(using Context): CaptureSet = - defn.captureRoot.termRef.readOnly.singletonCaptureSet + root.cap.readOnly.singletonCaptureSet /** Used as a recursion brake */ @sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala deleted file mode 100644 index c3be58879ccc..000000000000 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ /dev/null @@ -1,211 +0,0 @@ -package dotty.tools -package dotc -package cc - -import core.* -import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* -import StdNames.nme -import ast.tpd.* -import Decorators.* -import typer.ErrorReporting.errorType -import Names.TermName -import NameKinds.ExistentialBinderName -import NameOps.isImpureFunction -import CaptureSet.IdempotentCaptRefMap -import reporting.Message -import util.{SimpleIdentitySet, EqHashMap} -import util.Spans.NoSpan - -/** - -Handling existentials in CC: - - - We generally use existentials only in function and method result types - - All occurrences of an EX-bound variable appear co-variantly in the bound type - -In Setup: - - - Convert occurrences of `cap` in function results to existentials. Precise rules below. - - Conversions are done in two places: - - + As part of mapping from local types of parameters and results to infos of methods. - The local types just use `cap`, whereas the result type in the info uses EX-bound variables. - + When converting functions or methods appearing in explicitly declared types. - Here again, we only replace cap's in fucntion results. - - - Conversion is done with a BiTypeMap in `Existential.mapCap`. - -In reckeckApply and recheckTypeApply: - - - If an EX is toplevel in the result type, replace its bound variable - occurrences with `cap`. - -Level checking and avoidance: - - - Environments, capture refs, and capture set variables carry levels - - + levels start at 0 - + The level of a block or template statement sequence is one higher than the level of - its environment - + The level of a TermRef is the level of the environment where its symbol is defined. - + The level of a ThisType is the level of the statements of the class to which it beloongs. - + The level of a TermParamRef is currently -1 (i.e. TermParamRefs are not yet checked using this system) - + The level of a capture set variable is the level of the environment where it is created. - - - Variables also carry info whether they accept `cap` or not. Variables introduced under a box - don't, the others do. - - - Capture set variables do not accept elements of level higher than the variable's level - - - We use avoidance to heal such cases: If the level-incorrect ref appears - + covariantly: widen to underlying capture set, reject if that is cap and the variable does not allow it - + contravariantly: narrow to {} - + invarianty: reject with error - -In cv-computation (markFree): - - - Reach capabilities x* of a parameter x cannot appear in the capture set of - the owning method. They have to be widened to dcs(x), or, where this is not - possible, it's an error. - -In box adaptation: - - - Check that existential variables are not boxed or unboxed. - -Subtype rules - - - new alphabet: existentially bound variables `a`. - - they can be stored in environments Gamma. - - they are alpha-renable, usual hygiene conditions apply - - Gamma |- EX a.T <: U - if Gamma, a |- T <: U - - Gamma |- T <: EX a.U - if exists capture set C consisting of capture refs and ex-bound variables - bound in Gamma such that Gamma |- T <: [a := C]U - -Representation: - - EX a.T[a] is represented as a dependent function type - - (a: Exists) => T[a]] - - where Exists is defined in caps like this: - - sealed trait Exists extends Capability - - The defn.RefinedFunctionOf extractor will exclude existential types from - its results, so only normal refined functions match. - - Let `boundvar(ex)` be the TermParamRef defined by the existential type `ex`. - -Subtype checking algorithm, general scheme: - - Maintain two structures in TypeComparer: - - openExistentials: List[TermParamRef] - assocExistentials: Map[TermParamRef, List[TermParamRef]] - - `openExistentials` corresponds to the list of existential variables stored in the environment. - `assocExistentials` maps existential variables bound by existentials appearing on the right - to the value of `openExistentials` at the time when the existential on the right was dropped. - -Subtype checking algorithm, steps to add for tp1 <:< tp2: - - If tp1 is an existential EX a.tp1a: - - val saved = openExistentials - openExistentials = boundvar(tp1) :: openExistentials - try tp1a <:< tp2 - finally openExistentials = saved - - If tp2 is an existential EX a.tp2a: - - val saved = assocExistentials - assocExistentials = assocExistentials + (boundvar(tp2) -> openExistentials) - try tp1 <:< tp2a - finally assocExistentials = saved - - If tp2 is an existentially bound variable: - assocExistentials(tp2).isDefined - && (assocExistentials(tp2).contains(tp1) || tp1 is not existentially bound) - -Subtype checking algorithm, comparing two capture sets CS1 <:< CS2: - - We need to map the (possibly to-be-added) existentials in CS1 to existentials - in CS2 so that we can compare them. We use `assocExistentals` for that: - To map an EX-variable V1 in CS1, pick the last (i.e. outermost, leading to the smallest - type) EX-variable in `assocExistentials` that has V1 in its possible instances. - To go the other way (and therby produce a BiTypeMap), map an EX-variable - V2 in CS2 to the first (i.e. innermost) EX-variable it can be instantiated to. - If either direction is not defined, we choose a special "bad-existetal" value - that represents and out-of-scope existential. This leads to failure - of the comparison. - -Existential source syntax: - - Existential types are ususally not written in source, since we still allow the `^` - syntax that can express most of them more concesely (see below for translation rules). - But we should also allow to write existential types explicity, even if it ends up mainly - for debugging. To express them, we use the encoding with `Exists`, so a typical - expression of an existential would be - - (x: Exists) => A ->{x} B - - Existential types can only at the top level of the result type - of a function or method. - -Restrictions on Existential Types: (to be implemented if we want to -keep the source syntax for users). - - - An existential capture ref must be the only member of its set. This is - intended to model the idea that existential variables effectibely range - over capture sets, not capture references. But so far our calculus - and implementation does not yet acoommodate first-class capture sets. - - Existential capture refs must appear co-variantly in their bound type - - So the following would all be illegal: - - EX x.C^{x, io} // error: multiple members - EX x.() => EX y.C^{x, y} // error: multiple members - EX x.C^{x} ->{x} D // error: contra-variant occurrence - EX x.Set[C^{x}] // error: invariant occurrence - -Expansion of ^: - - We expand all occurrences of `cap` in the result types of functions or methods - to existentially quantified types. Nested scopes are expanded before outer ones. - - The expansion algorithm is then defined as follows: - - 1. In a result type, replace every occurrence of ^ with a fresh existentially - bound variable and quantify over all variables such introduced. - - 2. After this step, type aliases are expanded. If aliases have aliases in arguments, - the outer alias is expanded before the aliases in the arguments. Each time an alias - is expanded that reveals a `^`, apply step (1). - - 3. The algorithm ends when no more alieases remain to be expanded. - - Examples: - - - `A => B` is an alias type that expands to `(A -> B)^`, therefore - `() -> A => B` expands to `() -> EX c. A ->{c} B`. - - - `() => Iterator[A => B]` expands to `() => EX c. Iterator[A ->{c} B]` - - - `A -> B^` expands to `A -> EX c.B^{c}`. - - - If we define `type Fun[T] = A -> T`, then `() -> Fun[B^]` expands to `() -> EX c.Fun[B^{c}]`, which - dealiases to `() -> EX c.A -> B^{c}`. - - - If we define - - type F = A -> Fun[B^] - - then the type alias expands to - - type F = A -> EX c.A -> B^{c} -*/ -object Existential \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala deleted file mode 100644 index a91222f96cdd..000000000000 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ /dev/null @@ -1,26 +0,0 @@ -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, emptyRefs, NarrowingCapabilityMap} -import dotty.tools.dotc.util.SimpleIdentitySet - -/** A module for handling Fresh types. Fresh instances are top types that keep - * track of what they hide when capabilities get widened by subsumption to fresh. - * The module implements operations to convert between regular caps.cap and - * Fresh instances. root.Fresh(...) is encoded as `caps.cap @freshCapability(...)` where - * `freshCapability(...)` is a special kind of annotation of type `root.Annot` - * that contains a hidden set. - */ -object Fresh \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 36843e155037..6d52ad94613b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -373,7 +373,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: */ def stripImpliedCaptureSet(tp: Type): Type = tp match case tp @ CapturingType(parent, refs) - if (refs eq defn.universalCSImpliedByCapability) && !tp.isBoxedCapturing => + if (refs eq CaptureSet.universalImpliedByCapability) && !tp.isBoxedCapturing => parent case _ => tp @@ -401,7 +401,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: && !t.isSingleton && (!sym.isConstructor || (t ne tp.finalResultType)) // Don't add ^ to result types of class constructors deriving from Capability - then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) + then CapturingType(t, CaptureSet.universalImpliedByCapability, boxed = false) else normalizeCaptures(mapFollowingAliases(t)) def innerApply(t: Type) = diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 82dea9616017..cdc3afff8a2d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -132,7 +132,7 @@ object Synthetics: val (pt: PolyType) = info: @unchecked val (mt: MethodType) = pt.resType: @unchecked val (enclThis: ThisType) = owner.thisType: @unchecked - val paramCaptures = CaptureSet(enclThis, defn.captureRoot.termRef) + val paramCaptures = CaptureSet(enclThis, root.cap) pt.derivedLambdaType(resType = MethodType(mt.paramNames)( mt1 => mt.paramInfos.map(_.capturing(paramCaptures)), mt1 => CapturingType(mt.resType, CaptureSet(enclThis, mt1.paramRefs.head)))) @@ -150,7 +150,7 @@ object Synthetics: def transformCompareCaptures = val (enclThis: ThisType) = symd.owner.thisType: @unchecked MethodType( - defn.ObjectType.capturing(CaptureSet(defn.captureRoot.termRef, enclThis)) :: Nil, + defn.ObjectType.capturing(CaptureSet(root.cap, enclThis)) :: Nil, defn.BooleanType) symd.copySymDenotation(info = symd.name match diff --git a/compiler/src/dotty/tools/dotc/cc/root.scala b/compiler/src/dotty/tools/dotc/cc/root.scala index df3bb5f1decd..da4ee330ca3c 100644 --- a/compiler/src/dotty/tools/dotc/cc/root.scala +++ b/compiler/src/dotty/tools/dotc/cc/root.scala @@ -17,11 +17,53 @@ import util.{SimpleIdentitySet, EqHashMap} import util.Spans.NoSpan import annotation.internal.sharable +/** A module defining three kinds of root capabilities + * - `cap` of kind `Global`: This is the global root capability. Among others it is + * used in the types of formal parameters, in type bounds, and in self types. + * `cap` does not subsume other capabilities, except in arguments of + * `withCapAsRoot` calls. + * - Instances of Fresh(hidden), of kind Fresh. These do subsume other capabilties in scope. + * They track with hidden sets which other capabilities were subsumed. + * Hidden sets are inspected by separation checking. + * - Instances of Result(binder), of kind Result. These are existentials associated with + * the result types of dependent methods. They don't subsume other capabilties. + * + * Representation: + * + * - `cap` is just the TermRef `scala.caps.cap` defined in the `caps` module + * - `Fresh` and `Result` instances are annotated types of `scala.caps.cap` + * with a special `root.Annot` annotation. The symbol of the annotation is + * `annotation.internal.rootCapability`. The annotation carries a kind, which provides + * a hidden set for Fresh instances and a binder method type for Result instances. + * + * Setup: + * + * In the setup phase, `cap` instances in the result of a dependent function type + * or method type such as `(x: T): C^{cap}` are converted to `Result(binder)` instances, + * where `binder` refers to the method type. Most other cap instances are mapped to + * Fresh instances instead. For example the `cap` in the result of `T => C^{cap}` + * is mapped to a Fresh instance. + * + * If one needs to use a dependent function type yet one still want to map `cap` to + * a fresh instance instead an existential root, one can achieve that by the use + * of a type alias. For instance, the following type creates an existential for `^`: + * + * (x: A) => (C^{x}, D^) + * + * By contrast, this variant creates a fresh instance instead: + * + * type F[X] = (x: A) => (C^{x}, X) + * F[D^] + * + * The trick is that the argument D^ is mapped to D^{fresh} before the `F` alias + * is expanded. + */ object root: enum Kind: case Result(binder: MethodType) case Fresh(hidden: CaptureSet.HiddenSet) + case Global override def equals(other: Any): Boolean = (this eq other.asInstanceOf[AnyRef]) || this.match @@ -31,6 +73,7 @@ object root: case Kind.Fresh(h1) => other match case Kind.Fresh(h2) => h1 eq h2 case _ => false + case Kind.Global => false end Kind @sharable private var rootId = 0 @@ -43,7 +86,7 @@ object root: rootId += 1 rootId - override def symbol(using Context) = defn.FreshCapabilityAnnot + override def symbol(using Context) = defn.RootCapabilityAnnot override def tree(using Context) = New(symbol.typeRef, Nil) override def derivedAnnotation(tree: Tree)(using Context): Annotation = this @@ -63,6 +106,9 @@ object root: case Annot(kind) => this.kind eq kind case _ => false + /** Special treatment of `SubstBindingMaps` which can change the binder of a + * Result instances + */ override def mapWith(tm: TypeMap)(using Context) = kind match case Kind.Result(binder) => tm match case tm: Substituters.SubstBindingMap[MethodType] @unchecked if tm.from eq binder => @@ -71,6 +117,8 @@ object root: case _ => this end Annot + def cap(using Context): TermRef = defn.captureRoot.termRef + /** The type of fresh references */ type Fresh = AnnotatedType @@ -79,12 +127,12 @@ object root: private def make(owner: Symbol)(using Context): CaptureRef = if ccConfig.useSepChecks then val hiddenSet = CaptureSet.HiddenSet(owner) - val res = AnnotatedType(defn.captureRoot.termRef, Annot(Kind.Fresh(hiddenSet))) + val res = AnnotatedType(cap, Annot(Kind.Fresh(hiddenSet))) hiddenSet.owningCap = res //assert(hiddenSet.id != 3) res else - defn.captureRoot.termRef + cap def withOwner(owner: Symbol)(using Context): CaptureRef = make(owner) def apply()(using Context): CaptureRef = make(NoSymbol) @@ -100,7 +148,7 @@ object root: object Result: def apply(binder: MethodType)(using Context): Result = val hiddenSet = CaptureSet.HiddenSet(NoSymbol) - val res = AnnotatedType(defn.captureRoot.termRef, Annot(Kind.Result(binder))) + val res = AnnotatedType(cap, Annot(Kind.Result(binder))) hiddenSet.owningCap = res res @@ -109,10 +157,10 @@ object root: case _ => None end Result - def unapply(root: AnnotatedType)(using Context): Option[Annot] = - root.annot match - case ann: Annot => Some(ann) - case _ => None + def unapply(root: CaptureRef)(using Context): Option[Kind] = root match + case root @ AnnotatedType(_, ann: Annot) => Some(ann.kind) + case _ if root.isCap => Some(Kind.Global) + case _ => None /** Map each occurrence of cap to a different Sep.Cap instance */ class CapToFresh(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: @@ -138,7 +186,7 @@ object root: lazy val inverse: BiTypeMap & FollowAliasesMap = new BiTypeMap with FollowAliasesMap: def apply(t: Type): Type = t match - case t @ Fresh(_) => defn.captureRoot.termRef + case t @ Fresh(_) => cap case t @ CapturingType(_, refs) => mapOver(t) case _ => mapFollowingAliases(t) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 7b36ddee58f3..cf2795648484 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -15,7 +15,7 @@ import Comments.{Comment, docCtx} import util.Spans.NoSpan import config.Feature import Symbols.requiredModuleRef -import cc.{CaptureSet, RetainingType, Existential, readOnly} +import cc.{CaptureSet, RetainingType, readOnly} import ast.tpd.ref import scala.annotation.tailrec @@ -1073,7 +1073,6 @@ class Definitions { @tu lazy val UseAnnot: ClassSymbol = requiredClass("scala.caps.use") @tu lazy val ConsumeAnnot: ClassSymbol = requiredClass("scala.caps.consume") @tu lazy val RefineOverrideAnnot: ClassSymbol = requiredClass("scala.caps.refineOverride") - @tu lazy val ExistentialAnnot: ClassSymbol = requiredClass("scala.caps.existential") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature") @tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter") @@ -1089,7 +1088,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 RootCapabilityAnnot = requiredClass("scala.caps.rootCapability") @tu lazy val ReadOnlyCapabilityAnnot = requiredClass("scala.annotation.internal.readOnlyCapability") @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @@ -1561,7 +1560,7 @@ class Definitions { Set(StringClass, NothingClass, NullClass) ++ ScalaValueClasses() @tu lazy val capabilityWrapperAnnots: Set[Symbol] = - Set(ReachCapabilityAnnot, ReadOnlyCapabilityAnnot, MaybeCapabilityAnnot, FreshCapabilityAnnot) + Set(ReachCapabilityAnnot, ReadOnlyCapabilityAnnot, MaybeCapabilityAnnot, RootCapabilityAnnot) @tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0).asInstanceOf[Array[TypeRef]] val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(AbstractFunctionType.map(_.symbol.asClass)) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 1eecdd86cb0c..cc5ed21db673 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4076,7 +4076,7 @@ object Types extends TypeUtils { case ReachCapability(tp1) => apply(tp1) match case tp1a: CaptureRef if tp1a.isTrackableRef => tp1a.reach - case _ => defn.captureRoot.termRef + case _ => root.cap case AnnotatedType(parent, ann) if ann.refersToParamOf(thisLambdaType) => val parent1 = mapOver(parent) if ann.symbol.isRetainsLike then diff --git a/library/src/scala/annotation/internal/freshCapability.scala b/library/src/scala/annotation/internal/freshCapability.scala deleted file mode 100644 index 210220ec0a89..000000000000 --- a/library/src/scala/annotation/internal/freshCapability.scala +++ /dev/null @@ -1,10 +0,0 @@ -package scala.annotation -package internal - -/** An annotation used internally for fresh capability wrappers of `cap`. - * A fresh capability is encoded as `caps.cap @freshCapability(...)` where - * `freshCapability(...)` is a special kind of annotation of type `Fresh.Annot` - * that contains a hidden set. - */ -class freshCapability extends StaticAnnotation - diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 0ef53595fcc1..d53a9b128e5b 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -51,9 +51,8 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ extension (x: Any) def readOnlyCapability: Any = x - /** A trait to allow expressing existential types such as - * - * (x: Exists) => A ->{x} B + /** A trait that used to allow expressing existential types. Replaced by + * root.Result instances. */ @deprecated sealed trait Exists extends Capability @@ -89,20 +88,14 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class refineOverride extends annotation.StaticAnnotation - /** An internal annotation placed on a reference to an existential capability. - * That way, we can distinguish different universal capabilties referring to - * the same binder. For instance, - * - * () -> (Ref^, Ref^) - * - * would be encoded as - * - * () -> (ex: Exists) -> (Ref^{ex @ existential}, Ref^{ex @existential}) - * - * The two capture references are different since they carry two separately - * allocated annotations. + /** An annotation used internally for root capability wrappers of `cap` that + * represent either Fresh or Result capabilities. + * A capability is encoded as `caps.cap @rootCapability(...)` where + * `rootCapability(...)` is a special kind of annotation of type `root.Annot` + * that contains either a hidden set for Fresh instances or a method type binder + * for Result instances. */ - final class existential extends annotation.StaticAnnotation + final class rootCapability extends annotation.StaticAnnotation object unsafe: From 9a49c526d5960362fdf981e30c5a86b572053b79 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 11 Mar 2025 17:54:32 +0100 Subject: [PATCH 87/93] Add `mut` modifier to Parser doc comment --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 39ad8fc9a5e8..c24dbce1b6ac 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3393,7 +3393,8 @@ object Parsers { * | override * | opaque * LocalModifier ::= abstract | final | sealed | open | implicit | lazy | erased | - * inline | transparent | infix + * inline | transparent | infix | + * mut -- under cc */ def modifiers(allowed: BitSet = modifierTokens, start: Modifiers = Modifiers()): Modifiers = { @tailrec From d781b3e179204af6fb6066c3078f7c3a73e0cd4e Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 11 Mar 2025 17:54:52 +0100 Subject: [PATCH 88/93] Rename -Ycc-print-fresh to -Ycc-verbose --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 2 +- compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 5477767f6ba9..d2d716769990 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -452,7 +452,7 @@ private sealed trait YSettings: val YccDebug: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references.") val YccNew: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-new", "Used in conjunction with captureChecking language import, try out new variants (debug option)") val YccLog: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-log", "Used in conjunction with captureChecking language import, print tracing and debug info") - val YccPrintFresh: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-print-fresh", "Print hidden sets of fresh `cap` instances") + val YccVerbose: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-verbose", "Print root capabilities with more details") val YccPrintSetup: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-print-setup", "Used in conjunction with captureChecking language import, print trees after cc.Setup phase") /** Area-specific debug output */ diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index b8e8bdc660c8..41278ca27159 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -28,10 +28,10 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def printDebug = ctx.settings.YprintDebug.value /** Print Fresh instances as */ - protected def printFreshDetailed = ctx.settings.YccPrintFresh.value + protected def ccVerbose = ctx.settings.YccVerbose.value /** Print Fresh instances as "fresh" */ - protected def printFresh = printFreshDetailed || ctx.property(PrintFresh).isDefined + protected def printFresh = ccVerbose || ctx.property(PrintFresh).isDefined private var openRecs: List[RecType] = Nil @@ -464,7 +464,7 @@ class PlainPrinter(_ctx: Context) extends Printer { vbleText ~ hashStr(binder) ~ Str(idStr).provided(showUniqueIds) case tp @ root.Fresh(hidden) => val idStr = if showUniqueIds then s"#${tp.rootAnnot.id}" else "" - if printFreshDetailed then s"" + if ccVerbose then s"" else if printFresh then "fresh" else "cap" case tp => toText(tp) From dc0dd7fe4e4460b4024ead8d7a0ece4dbc4d6197 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 12 Mar 2025 19:05:02 +0100 Subject: [PATCH 89/93] Refactor addenda generation Avoid globally visible vars in CCState. Make sure notes don't leak from one test to another. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 35 +++++++--- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 5 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 40 +++++------ .../dotty/tools/dotc/cc/CheckCaptures.scala | 68 +++++++++---------- 4 files changed, 77 insertions(+), 71 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 57c91704bd7d..7b8570f7f492 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -16,7 +16,7 @@ import config.Feature import collection.mutable import CCState.* import reporting.Message -import CaptureSet.VarState +import CaptureSet.{VarState, CompareResult} /** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() @@ -76,19 +76,32 @@ def depFun(args: List[Type], resultType: Type, isContextual: Boolean, paramNames /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type)(using Context) extends Exception(tpe.show) +/** A base trait for data producing addenda to error messages */ +trait ErrorNote + /** Capture checking state, which is known to other capture checking components */ class CCState: - /** The last pair of capture reference and capture set where - * the reference could not be added to the set due to a level conflict. - */ - var levelError: Option[CaptureSet.CompareResult.LevelError] = None - - /** Optionally, a pair of an existential variable and another capability. - * Set when a subsumes check decides that an existential variable cannot be - * instantiated to the other capability. - */ - var existentialSubsumesFailure: Option[(CaptureRef, CaptureRef)] = None + /** Error reprting notes produces since the last call to `test` */ + var notes: List[ErrorNote] = Nil + + def addNote(note: ErrorNote): Unit = + if !notes.exists(_.getClass == note.getClass) then + notes = note :: notes + + def test(op: => CompareResult): CompareResult = + val saved = notes + notes = Nil + try op.withNotes(notes) + finally notes = saved + + def testOK(op: => Boolean): CompareResult = + val saved = notes + notes = Nil + try + if op then CompareResult.OK + else CompareResult.Fail(Nil).withNotes(notes) + finally notes = saved /** Warnings relating to upper approximations of capture sets with * existentially bound variables. diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index dba10e1fb9f4..0b810218c07c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -253,11 +253,10 @@ trait CaptureRef extends TypeProxy, ValueType: case root.Fresh(hidden) => vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) || !y.stripReadOnly.isCap && !yIsExistential && canAddHidden && vs.addHidden(hidden, y) - case root.Result(binder) => + case x @ root.Result(binder) => if y.derivesFromSharedCapability then true else - ccState.existentialSubsumesFailure = - ccState.existentialSubsumesFailure.orElse(Some(this, y)) + ccState.addNote(CaptureSet.ExistentialSubsumesFailure(x, y)) false case _ => y match diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 05dff8e05875..90fa5b90f25f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -233,7 +233,7 @@ sealed abstract class CaptureSet extends Showable: if result.isOK then addDependent(that) else - ccState.levelError = ccState.levelError.orElse(result.levelError) + result.levelError.foreach(ccState.addNote) varState.rollBack() result //.showing(i"subcaptures $this <:< $that = ${result.show}", capt) @@ -1077,10 +1077,16 @@ object CaptureSet: /** A TypeMap that is the identity on capture references */ trait IdentityCaptRefMap extends TypeMap + /** A value of this class is produced and added as a note to ccState + * when a subsumes check decides that an existential variable `ex` cannot be + * instantiated to the other capability `other`. + */ + case class ExistentialSubsumesFailure(val ex: root.Result, val other: CaptureRef) extends ErrorNote + enum CompareResult extends Showable: case OK case Fail(trace: List[CaptureSet]) - case LevelError(cs: CaptureSet, elem: CaptureRef) + case LevelError(cs: CaptureSet, elem: CaptureRef) extends CompareResult, ErrorNote override def toText(printer: Printer): Text = inContext(printer.printerContext): @@ -1105,6 +1111,16 @@ object CaptureSet: case result: LevelError => Some(result) case _ => None + private var myErrorNotes: List[ErrorNote] = Nil + + def errorNotes: List[ErrorNote] = myErrorNotes + + def withNotes(notes: List[ErrorNote]): CompareResult = this match + case OK => OK + case _ => + myErrorNotes = notes + this + inline def andAlso(op: Context ?=> CompareResult)(using Context): CompareResult = if isOK then op else this @@ -1412,24 +1428,4 @@ object CaptureSet: println(i" ${cv.show.padTo(20, ' ')} :: ${cv.deps.toList}%, %") } else op - - def levelErrors: Addenda = new Addenda: - override def toAdd(using Context) = - for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield - ccState.levelError = None - if ref.stripReadOnly.isCapOrFresh then - def capStr = if ref.isReadOnly then "cap.rd" else "cap" - i""" - | - |Note that the universal capability `$capStr` - |cannot be included in capture set $cs""" - else - val levelStr = ref match - case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}" - case _ => "" - i""" - | - |Note that reference ${ref}$levelStr - |cannot be included in outer capture set $cs""" - end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 9841a80b9e8e..5a0254f710cd 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, ExistentialSubsumesFailure} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} @@ -354,7 +354,7 @@ class CheckCaptures extends Recheck, SymTransformer: def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = if !res.isOK then inContext(root.printContext(added, res.blocking)): - def toAdd: String = CaptureSet.levelErrors.toAdd.mkString + def toAdd: String = errorNotes(res.errorNotes).toAdd.mkString def descr: String = val d = res.blocking.description if d.isEmpty then provenance else "" @@ -363,7 +363,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), + ccState.test(elem.singletonCaptureSet.subCaptures(cs)), i"$elem cannot be referenced here; it is not", elem, pos, provenance) @@ -371,7 +371,7 @@ class CheckCaptures extends Recheck, SymTransformer: def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, provenance: => String = "", cs1description: String = "")(using Context) = checkOK( - cs1.subCaptures(cs2), + ccState.test(cs1.subCaptures(cs2)), if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not" else i"references $cs1$cs1description are not all", cs1, pos, provenance) @@ -1210,27 +1210,29 @@ class CheckCaptures extends Recheck, SymTransformer: type BoxErrors = mutable.ListBuffer[Message] | Null - private def boxErrorAddenda(boxErrors: BoxErrors) = - if boxErrors == null then NothingToAdd + private def errorNotes(notes: List[ErrorNote])(using Context): Addenda = + if notes.isEmpty then NothingToAdd else new Addenda: - override def toAdd(using Context): List[String] = - boxErrors.toList.map: msg => - i""" - | - |Note that ${msg.toString}""" - - private def existentialSubsumesFailureAddenda(using Context): Addenda = - ccState.existentialSubsumesFailure match - case Some((ex @ root.Result(binder), other)) => - new Addenda: - override def toAdd(using Context): List[String] = - val ann = ex.rootAnnot - i""" - | - |Note that the existential capture root in ${ex.rootAnnot.originalBinder.resType} + override def toAdd(using Context) = notes.map: note => + val msg = note match + case CompareResult.LevelError(cs, ref) => + if ref.stripReadOnly.isCapOrFresh then + def capStr = if ref.isReadOnly then "cap.rd" else "cap" + i"""the universal capability `$capStr` + |cannot be included in capture set $cs""" + else + val levelStr = ref match + case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}" + case _ => "" + i"""reference ${ref}$levelStr + |cannot be included in outer capture set $cs""" + case ExistentialSubsumesFailure(ex, other) => + i"""the existential capture root in ${ex.rootAnnot.originalBinder.resType} |cannot subsume the capability $other""" - :: Nil - case _ => NothingToAdd + i""" + | + |Note that ${msg.toString}""" + /** Addendas for error messages that show where we have under-approximated by * mapping a a capture ref in contravariant position to the empty set because @@ -1264,15 +1266,14 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type = var expected1 = alignDependentFunction(expected, actual.stripCapturing) - val boxErrors = new mutable.ListBuffer[Message] - val actualBoxed = adapt(actual, expected1, tree, boxErrors) + val actualBoxed = adapt(actual, expected1, tree) //println(i"check conforms $actualBoxed <<< $expected1") if actualBoxed eq actual then // Only `addOuterRefs` when there is no box adaptation expected1 = addOuterRefs(expected1, actual, tree.srcPos) - ccState.existentialSubsumesFailure = None - if isCompatible(actualBoxed, expected1) then + val result = ccState.testOK(isCompatible(actualBoxed, expected1)) + if result.isOK then if debugSuccesses then tree match case Ident(_) => println(i"SUCCESS $tree for $actual <:< $expected:\n${TypeComparer.explained(_.isSubType(actualBoxed, expected1))}") @@ -1283,10 +1284,7 @@ class CheckCaptures extends Recheck, SymTransformer: inContext(root.printContext(actualBoxed, expected1)): err.typeMismatch(tree.withType(actualBoxed), expected1, addApproxAddenda( - addenda - ++ CaptureSet.levelErrors - ++ boxErrorAddenda(boxErrors) - ++ existentialSubsumesFailureAddenda, + addenda ++ errorNotes(result.errorNotes), expected1)) actual end checkConformsExpr @@ -1397,7 +1395,7 @@ class CheckCaptures extends Recheck, SymTransformer: * * @param alwaysConst always make capture set variables constant after adaptation */ - def adaptBoxed(actual: Type, expected: Type, tree: Tree, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = + def adaptBoxed(actual: Type, expected: Type, tree: Tree, covariant: Boolean, alwaysConst: Boolean)(using Context): Type = def recur(actual: Type, expected: Type, covariant: Boolean): Type = @@ -1551,7 +1549,7 @@ class CheckCaptures extends Recheck, SymTransformer: * - narrow nested captures of `x`'s underlying type to `{x*}` * - do box adaptation */ - def adapt(actual: Type, expected: Type, tree: Tree, boxErrors: BoxErrors)(using Context): Type = + def adapt(actual: Type, expected: Type, tree: Tree)(using Context): Type = if noWiden(actual, expected) then actual else @@ -1559,7 +1557,7 @@ class CheckCaptures extends Recheck, SymTransformer: val improved = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( improved.withReachCaptures(actual), expected, tree, - covariant = true, alwaysConst = false, boxErrors) + covariant = true, alwaysConst = false) if adapted eq improvedVAR // no .rd improvement, no box-adaptation then actual // might as well use actual instead of improved widened else adapted.showing(i"adapt $actual vs $expected = $adapted", capt) @@ -1585,7 +1583,7 @@ class CheckCaptures extends Recheck, SymTransformer: try curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) val adapted = - adaptBoxed(actual, expected1, tree, covariant = true, alwaysConst = true, null) + adaptBoxed(actual, expected1, tree, covariant = true, alwaysConst = true) actual match case _: MethodType => // We remove the capture set resulted from box adaptation for method types, From fbe37b8a1e3d348089367a8ea75b6bc93450d80b Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 12 Mar 2025 22:44:15 +0100 Subject: [PATCH 90/93] More refactoring, avoid globally accessible variable --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 13 ++--- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 21 ++++---- .../dotty/tools/dotc/cc/CheckCaptures.scala | 48 ++++++++++--------- 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 7b8570f7f492..908e3174bfce 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -16,7 +16,7 @@ import config.Feature import collection.mutable import CCState.* import reporting.Message -import CaptureSet.{VarState, CompareResult} +import CaptureSet.{VarState, CompareResult, CompareFailure} /** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() @@ -92,16 +92,13 @@ class CCState: def test(op: => CompareResult): CompareResult = val saved = notes notes = Nil - try op.withNotes(notes) + try op match + case res: CompareFailure => res.withNotes(notes) + case res => res finally notes = saved def testOK(op: => Boolean): CompareResult = - val saved = notes - notes = Nil - try - if op then CompareResult.OK - else CompareResult.Fail(Nil).withNotes(notes) - finally notes = saved + test(if op then CompareResult.OK else CompareResult.Fail(Nil)) /** Warnings relating to upper approximations of capture sets with * existentially bound variables. diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 90fa5b90f25f..d9a48e90e6b6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1083,10 +1083,17 @@ object CaptureSet: */ case class ExistentialSubsumesFailure(val ex: root.Result, val other: CaptureRef) extends ErrorNote + trait CompareFailure: + private var myErrorNotes: List[ErrorNote] = Nil + def errorNotes: List[ErrorNote] = myErrorNotes + def withNotes(notes: List[ErrorNote]): this.type = + myErrorNotes = notes + this + enum CompareResult extends Showable: case OK - case Fail(trace: List[CaptureSet]) - case LevelError(cs: CaptureSet, elem: CaptureRef) extends CompareResult, ErrorNote + case Fail(trace: List[CaptureSet]) extends CompareResult, CompareFailure + case LevelError(cs: CaptureSet, elem: CaptureRef) extends CompareResult, CompareFailure, ErrorNote override def toText(printer: Printer): Text = inContext(printer.printerContext): @@ -1111,16 +1118,6 @@ object CaptureSet: case result: LevelError => Some(result) case _ => None - private var myErrorNotes: List[ErrorNote] = Nil - - def errorNotes: List[ErrorNote] = myErrorNotes - - def withNotes(notes: List[ErrorNote]): CompareResult = this match - case OK => OK - case _ => - myErrorNotes = notes - this - inline def andAlso(op: Context ?=> CompareResult)(using Context): CompareResult = if isOK then op else this diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 5a0254f710cd..461d54ee3b0b 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, ExistentialSubsumesFailure} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult, CompareFailure, ExistentialSubsumesFailure} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} @@ -352,13 +352,15 @@ class CheckCaptures extends Recheck, SymTransformer: /** If `res` is not CompareResult.OK, report an error */ def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = - if !res.isOK then - inContext(root.printContext(added, res.blocking)): - def toAdd: String = errorNotes(res.errorNotes).toAdd.mkString - def descr: String = - val d = res.blocking.description - if d.isEmpty then provenance else "" - report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) + res match + case res: CompareFailure => + inContext(root.printContext(added, res.blocking)): + def toAdd: String = errorNotes(res.errorNotes).toAdd.mkString + def descr: String = + val d = res.blocking.description + if d.isEmpty then provenance else "" + report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) + case _ => /** Check subcapturing `{elem} <: cs`, report error on failure */ def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = @@ -1272,21 +1274,21 @@ class CheckCaptures extends Recheck, SymTransformer: if actualBoxed eq actual then // Only `addOuterRefs` when there is no box adaptation expected1 = addOuterRefs(expected1, actual, tree.srcPos) - val result = ccState.testOK(isCompatible(actualBoxed, expected1)) - if result.isOK then - if debugSuccesses then tree match - case Ident(_) => - println(i"SUCCESS $tree for $actual <:< $expected:\n${TypeComparer.explained(_.isSubType(actualBoxed, expected1))}") - case _ => - actualBoxed - else - capt.println(i"conforms failed for ${tree}: $actual vs $expected") - inContext(root.printContext(actualBoxed, expected1)): - err.typeMismatch(tree.withType(actualBoxed), expected1, - addApproxAddenda( - addenda ++ errorNotes(result.errorNotes), - expected1)) - actual + ccState.testOK(isCompatible(actualBoxed, expected1)) match + case CompareResult.OK => + if debugSuccesses then tree match + case Ident(_) => + println(i"SUCCESS $tree for $actual <:< $expected:\n${TypeComparer.explained(_.isSubType(actualBoxed, expected1))}") + case _ => + actualBoxed + case fail: CompareFailure => + capt.println(i"conforms failed for ${tree}: $actual vs $expected") + inContext(root.printContext(actualBoxed, expected1)): + err.typeMismatch(tree.withType(actualBoxed), expected1, + addApproxAddenda( + addenda ++ errorNotes(fail.errorNotes), + expected1)) + actual end checkConformsExpr /** Turn `expected` into a dependent function when `actual` is dependent. */ From ac84d134188f70fde65bbe9d6fd1274dd1c17c7d Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Mar 2025 10:57:30 +0100 Subject: [PATCH 91/93] Fix typo in error message --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../neg-custom-args/captures/box-adapt-contra.check | 12 ++++++------ tests/neg-custom-args/captures/i15772.check | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 461d54ee3b0b..8b6ce6550cc3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1458,7 +1458,7 @@ class CheckCaptures extends Recheck, SymTransformer: 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""", tree.srcPos) + |since the additional capture set $leaked resulting from box conversion is not allowed in $actual""", tree.srcPos) cs def adaptedType(resultBoxed: Boolean) = diff --git a/tests/neg-custom-args/captures/box-adapt-contra.check b/tests/neg-custom-args/captures/box-adapt-contra.check index e7ca18e7a9af..fd36f23aa999 100644 --- a/tests/neg-custom-args/captures/box-adapt-contra.check +++ b/tests/neg-custom-args/captures/box-adapt-contra.check @@ -1,15 +1,15 @@ -- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:9:52 --------------------------------------------------- 9 | val f: (Cap^{c} -> Unit) -> Unit = useCap[Cap^{c}](c) // error | ^^^^^^^^^^^^^^^^^^ - | Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{c} Unit - | since the additional capture set {c} resulted from box conversion is not allowed in box Cap^{c} -> Unit + | Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{c} Unit + | since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} -> Unit -- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:13:57 -------------------------------------------------- 13 | val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // error, was ok when cap was a root | ^^^^^^^^^^^^^^^^^^^ - | Cap^{c} => Unit cannot be box-converted to box Cap^{c} ->{cap, c} Unit - | since the additional capture set {c} resulted from box conversion is not allowed in box Cap^{c} => Unit + | Cap^{c} => Unit cannot be box-converted to box Cap^{c} ->{cap, c} Unit + | since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} => Unit -- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:19:54 -------------------------------------------------- 19 | val f3: (Cap^{c} -> Unit) => Unit = useCap3[Cap^{c}](c) // error | ^^^^^^^^^^^^^^^^^^^ - | Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{d, c} Unit - | since the additional capture set {c} resulted from box conversion is not allowed in box Cap^{c} ->{d} Unit + | Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{d, c} Unit + | since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} ->{d} Unit diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 15eb808972bd..b867636a64cd 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -7,7 +7,7 @@ 22 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error | ^^^^^^^ |C^ => Unit cannot be box-converted to box C{val arg: C^}^{c} ->{cap, c} Unit - |since the additional capture set {c} resulted from box conversion is not allowed in box C{val arg: C^}^{c} => Unit + |since the additional capture set {c} resulting from box conversion is not allowed in box C{val arg: C^}^{c} => Unit -- Error: tests/neg-custom-args/captures/i15772.scala:28:26 ------------------------------------------------------------ 28 | val c : C^{x} = new C(x) // error | ^ @@ -17,7 +17,7 @@ 29 | val boxed2 : Observe[C^] = box2(c) // error | ^^^^^^^ |C^ => Unit cannot be box-converted to box C{val arg: C^}^{c} ->{cap, c} Unit - |since the additional capture set {c} resulted from box conversion is not allowed in box C{val arg: C^}^{c} => Unit + |since the additional capture set {c} resulting from box conversion is not allowed in box C{val arg: C^}^{c} => Unit -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:35:34 --------------------------------------- 35 | val boxed2 : Observe[C]^ = box2(c) // error | ^ From 23cb1f3622f954e583eb8bc0b052ee7a9a880e27 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Mar 2025 13:58:20 +0100 Subject: [PATCH 92/93] Change rules for tracked refinments and tighten intersections Intersections used the heuristic mightAccountFor instead of the precise accountsFor. Thsi can lead to a loss of precision and (if unchecked afterwards) also soundness. The fix caused some tests to fail, which involved tracked parameters. We now deal with tracked parameters in the same way as parameters that carry a @refineOverride annotation. --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- compiler/src/dotty/tools/dotc/core/Types.scala | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index d9a48e90e6b6..688605dcc32d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -935,7 +935,7 @@ object CaptureSet: end Intersection def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = - cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + cs1.elems.filter(cs2.accountsFor) ++ cs2.elems.filter(cs1.accountsFor) /** A capture set variable used to record the references hidden by a Fresh instance, * The elems and deps members are repurposed as follows: diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index cc5ed21db673..df7700c73a17 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -860,10 +860,14 @@ object Types extends TypeUtils { pinfo recoverable_& rinfo pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, jointInfo) } - else rinfo match - case AnnotatedType(rinfo1, ann) if ann.symbol == defn.RefineOverrideAnnot => - pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, rinfo1) - case _ => + else + val overridingRefinement = rinfo match + case AnnotatedType(rinfo1, ann) if ann.symbol == defn.RefineOverrideAnnot => rinfo1 + case _ if pdenot.symbol.is(Tracked) => rinfo + case _ => NoType + if overridingRefinement.exists then + pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, overridingRefinement) + else val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] val joint = pdenot.meet( new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), From 48a52a244c29cc32775c7dcb04946d9e1c368d96 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 14 Mar 2025 11:34:14 +0100 Subject: [PATCH 93/93] Fix MimaFilters --- project/MiMaFilters.scala | 9 --------- 1 file changed, 9 deletions(-) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 3f225482b2c4..964acf50089f 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -8,16 +8,7 @@ object MiMaFilters { val ForwardsBreakingChanges: Map[String, Seq[ProblemFilter]] = Map( // Additions that require a new minor version of the library Build.mimaPreviousDottyVersion -> Seq( - ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.betterFors"), - ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$betterFors$"), - 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"), - ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.preview"), - ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.packageObjectValues"), - ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$packageObjectValues$"), ), // Additions since last LTS