From 6da745bf3c81bf76f0e813b2e2087e57591dd83b Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 6 Jul 2025 18:07:43 +0200 Subject: [PATCH 1/7] Simplification: Generate annotations for .rd and * directly No more detour via PostfixOps of compiler-generated names. --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 4 ---- compiler/src/dotty/tools/dotc/ast/untpd.scala | 6 ++++++ compiler/src/dotty/tools/dotc/core/StdNames.scala | 2 -- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 10 +++++----- .../src/dotty/tools/dotc/printing/RefinedPrinter.scala | 7 +------ 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index d71a6329e8b0..ebe166e29f4b 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -2268,10 +2268,6 @@ object desugar { Annotated( AppliedTypeTree(ref(defn.SeqType), t), New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil)) - else if op.name == nme.CC_REACH then - Annotated(t, New(ref(defn.ReachCapabilityAnnot.typeRef), Nil :: Nil)) - else if op.name == nme.CC_READONLY then - Annotated(t, New(ref(defn.ReadOnlyCapabilityAnnot.typeRef), Nil :: 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/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 4198c78e3288..8006f1c510be 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -550,6 +550,12 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { annot.putAttachment(RetainsAnnot, ()) Annotated(parent, annot) + def makeReachAnnot()(using Context): Tree = + New(ref(defn.ReachCapabilityAnnot.typeRef), Nil :: Nil) + + def makeReadOnlyAnnot()(using Context): Tree = + New(ref(defn.ReadOnlyCapabilityAnnot.typeRef), Nil :: Nil) + def makeConstructor(tparams: List[TypeDef], vparamss: List[List[ValDef]], rhs: Tree = EmptyTree)(using Context): DefDef = DefDef(nme.CONSTRUCTOR, joinParams(tparams, vparamss), TypeTree(), rhs) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9352be725e2c..5c4b8629e43a 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -120,8 +120,6 @@ object StdNames { val BITMAP_TRANSIENT: N = s"${BITMAP_PREFIX}trans$$" // initialization bitmap for transient lazy vals 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$" diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index e4314b27a32c..da5a59c52661 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1593,20 +1593,20 @@ object Parsers { */ def captureRef(): Tree = - def derived(ref: Tree, name: TermName) = + def derived(ref: Tree, ann: () => Tree) = in.nextToken() - atSpan(startOffset(ref)) { PostfixOp(ref, Ident(name)) } + atSpan(startOffset(ref)) { Annotated(ref, ann()) } def recur(ref: Tree): Tree = if in.token == DOT then in.nextToken() - if in.isIdent(nme.rd) then derived(ref, nme.CC_READONLY) + if in.isIdent(nme.rd) then derived(ref, makeReadOnlyAnnot) else recur(selector(ref)) else if in.isIdent(nme.raw.STAR) then - val reachRef = derived(ref, nme.CC_REACH) + val reachRef = derived(ref, makeReachAnnot) if in.token == DOT && in.lookahead.isIdent(nme.rd) then in.nextToken() - derived(reachRef, nme.CC_READONLY) + derived(reachRef, makeReadOnlyAnnot) else reachRef else ref diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 2578bbba6316..1e58268c2e38 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -762,12 +762,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { val opPrec = parsing.precedence(op.name) changePrec(opPrec) { toText(l) ~ " " ~ toText(op) ~ " " ~ toText(r) } 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) } + changePrec(InfixPrec) { toText(l) ~ " " ~ toText(op) } case PrefixOp(op, r) => changePrec(DotPrec) { toText(op) ~ " " ~ toText(r) } case Parens(t) => From 4c95df46bba50ab195bb299c93341a1de51e2b77 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 7 Jul 2025 16:24:48 +0200 Subject: [PATCH 2/7] Rename SharedCapability to Sharable and add Control capability class --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../src/dotty/tools/dotc/cc/SepCheck.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 ++-- .../dotty/tools/dotc/core/Definitions.scala | 2 +- library/src/scala/CanThrow.scala | 2 +- library/src/scala/caps/package.scala | 8 +++++++- .../captures/capt-capability.scala | 4 ++-- .../captures/cc-existential-conformance.check | 4 ++-- .../captures/cc-poly-source.scala | 2 +- .../captures/effect-swaps-explicit.scala | 2 +- .../captures/effect-swaps.scala | 2 +- .../captures/erased-methods2.check | 4 ++-- .../captures/heal-tparam-cs.check | 2 +- tests/neg-custom-args/captures/i16226.check | 2 +- tests/neg-custom-args/captures/reaches.check | 2 +- .../neg-custom-args/captures/scoped-caps.check | 18 +++++++++--------- .../neg-custom-args/captures/scoped-caps.scala | 2 +- .../captures/shared-capability.scala | 4 ++-- .../captures/capt-capability.scala | 4 ++-- .../captures/cc-poly-source-capability.scala | 4 ++-- tests/pos-custom-args/captures/i16226.scala | 2 +- .../captures/reach-capability.scala | 4 ++-- .../stdlibExperimentalDefinitions.scala | 3 ++- 23 files changed, 46 insertions(+), 39 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index dccbd0a005d7..1275c1b38eb6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1290,7 +1290,7 @@ class CheckCaptures extends Recheck, SymTransformer: case ExistentialSubsumesFailure(ex, other) => def since = if other.isTerminalCapability then "" - else " since that capability is not a SharedCapability" + else " since that capability is not a `Sharable` capability" i"""the existential capture root in ${ex.originalBinder.resType} |cannot subsume the capability $other$since""" case MutAdaptFailure(cs, lo, hi) => diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 4a391bf2c62f..f94a8172cd11 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -597,7 +597,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * - 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. + * References that extend cpas.Sharable are excluded from checking. * As a side effect, add all checked references with the given position `pos` * to the global `consumed` map. * diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 6143a7131e32..f686ac60298a 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -375,10 +375,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else fntpe /** 1. Check that parents of capturing types are not pure. - * 2. Check that types extending SharedCapability don't have a `cap` in their capture set. + * 2. Check that types extending caps.Sharable 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 + * where some prefix derives from Sharable. 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. diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 381caa775dbd..e70e5690f32f 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1011,7 +1011,7 @@ class Definitions { @tu lazy val Caps_ContainsModule: Symbol = requiredModule("scala.caps.Contains") @tu lazy val Caps_containsImpl: TermSymbol = Caps_ContainsModule.requiredMethod("containsImpl") @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") - @tu lazy val Caps_SharedCapability: ClassSymbol = requiredClass("scala.caps.SharedCapability") + @tu lazy val Caps_SharedCapability: ClassSymbol = requiredClass("scala.caps.Sharable") @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index 485dcecb37df..f6f9bcf40006 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.SharedCapability +erased class CanThrow[-E <: Exception] extends caps.Control @experimental object unsafeExceptions: diff --git a/library/src/scala/caps/package.scala b/library/src/scala/caps/package.scala index fedfd7400e25..e787cb0af16b 100644 --- a/library/src/scala/caps/package.scala +++ b/library/src/scala/caps/package.scala @@ -37,7 +37,13 @@ trait Mutable extends Capability * During separation checking, shared capabilities are not taken into account. */ @experimental -trait SharedCapability extends Capability +trait Sharable extends Capability + +/** Base trait for capabilities that capture some continuation or return point in + * the stack. Examples are exceptions, labels, Async, CanThrow. + */ +@experimental +trait Control extends Sharable /** Carrier trait for capture set type parameters */ @experimental diff --git a/tests/neg-custom-args/captures/capt-capability.scala b/tests/neg-custom-args/captures/capt-capability.scala index 7813ad8144b8..0f293872da25 100644 --- a/tests/neg-custom-args/captures/capt-capability.scala +++ b/tests/neg-custom-args/captures/capt-capability.scala @@ -1,7 +1,7 @@ -import caps.{Capability, SharedCapability} +import caps.{Capability, Sharable} def foo() = - val x: SharedCapability = ??? + val x: Sharable = ??? val z3 = if x == null then (y: Unit) => x else (y: Unit) => new Capability() {} // error diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.check b/tests/neg-custom-args/captures/cc-existential-conformance.check index a644b4c897df..549e1c0543b5 100644 --- a/tests/neg-custom-args/captures/cc-existential-conformance.check +++ b/tests/neg-custom-args/captures/cc-existential-conformance.check @@ -19,7 +19,7 @@ | where: ^ refers to a root capability associated with the result type of (x: A): B^ | | Note that the existential capture root in B^ - | cannot subsume the capability y* since that capability is not a SharedCapability + | cannot subsume the capability y* since that capability is not a `Sharable` capability | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:13:19 ------------------- @@ -43,6 +43,6 @@ | where: ^ refers to a root capability associated with the result type of (x: A): B^ | | Note that the existential capture root in B^ - | cannot subsume the capability y* since that capability is not a SharedCapability + | cannot subsume the capability y* since that capability is not a `Sharable` capability | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-poly-source.scala b/tests/neg-custom-args/captures/cc-poly-source.scala index fc47e6504810..915903d670e8 100644 --- a/tests/neg-custom-args/captures/cc-poly-source.scala +++ b/tests/neg-custom-args/captures/cc-poly-source.scala @@ -30,7 +30,7 @@ import caps.use 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 + // Label a Sharable, see cc-poly-source-capability.scala val src = Source[{lbls*}] for l <- listeners do src.register(l) diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.scala b/tests/neg-custom-args/captures/effect-swaps-explicit.scala index 33596772b9a0..56ab856a7782 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.scala +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.scala @@ -14,7 +14,7 @@ end boundary import boundary.{Label, break} -trait Async extends caps.SharedCapability +trait Async extends caps.Sharable object Async: def blocking[T](body: Async ?=> T): T = ??? diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index 06dca2bfa004..27d84d27e556 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -14,7 +14,7 @@ end boundary import boundary.{Label, break} -trait Async extends caps.SharedCapability +trait Async extends caps.Sharable object Async: def blocking[T](body: Async ?=> T): T = ??? diff --git a/tests/neg-custom-args/captures/erased-methods2.check b/tests/neg-custom-args/captures/erased-methods2.check index d7cca4635f20..8e163795f94b 100644 --- a/tests/neg-custom-args/captures/erased-methods2.check +++ b/tests/neg-custom-args/captures/erased-methods2.check @@ -9,7 +9,7 @@ | ^ refers to the universal root capability | |Note that the existential capture root in (erased x$2: CT[Ex2]^) ?=> Unit - |cannot subsume the capability x$1.type since that capability is not a SharedCapability + |cannot subsume the capability x$1.type since that capability is not a `Sharable` capability 21 | ?=> (x$2: CT[Ex2]^) 22 | ?=> 23 | //given (CT[Ex3]^) = x$1 @@ -28,7 +28,7 @@ | ^ refers to the universal root capability | |Note that the existential capture root in (erased x$1: CT[Ex2]^) ?=> (erased x$2: CT[Ex1]^) ?=> Unit - |cannot subsume the capability x$1.type since that capability is not a SharedCapability + |cannot subsume the capability x$1.type since that capability is not a `Sharable` capability 32 | ?=> (erased x$2: CT[Ex2]^) 33 | ?=> (erased x$3: CT[Ex1]^) 34 | ?=> 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 index d4a3734ff226..cfda44733b6e 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.check +++ b/tests/neg-custom-args/captures/heal-tparam-cs.check @@ -23,7 +23,7 @@ | ^ refers to the universal root capability | | Note that the existential capture root in () => Unit - | cannot subsume the capability x$0.type since that capability is not a SharedCapability + | cannot subsume the capability x$0.type since that capability is not a `Sharable` capability 16 | (c1: Capp^) => () => { c1.use() } 17 | } | diff --git a/tests/neg-custom-args/captures/i16226.check b/tests/neg-custom-args/captures/i16226.check index 6d59d362b464..1d79d29165dc 100644 --- a/tests/neg-custom-args/captures/i16226.check +++ b/tests/neg-custom-args/captures/i16226.check @@ -22,6 +22,6 @@ | ^ refers to a root capability associated with the result type of (ref: LazyRef[A]^{io}, f: A =>² B): LazyRef[B]^ | |Note that the existential capture root in LazyRef[B]^ - |cannot subsume the capability f1.type since that capability is not a SharedCapability + |cannot subsume the capability f1.type since that capability is not a `Sharable` capability | | 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 0d683cbaf1ca..9734845a2e31 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -75,7 +75,7 @@ | ^² refers to a root capability associated with the result type of (x: File^): File^² | | Note that the existential capture root in File^ - | cannot subsume the capability x.type since that capability is not a SharedCapability + | cannot subsume the capability x.type since that capability is not a `Sharable` capability | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:70:38 -------------------------------------- diff --git a/tests/neg-custom-args/captures/scoped-caps.check b/tests/neg-custom-args/captures/scoped-caps.check index b92464f8ce6f..65d9865a393c 100644 --- a/tests/neg-custom-args/captures/scoped-caps.check +++ b/tests/neg-custom-args/captures/scoped-caps.check @@ -19,7 +19,7 @@ | ^² refers to a root capability associated with the result type of (x: A^): B^² | | Note that the existential capture root in B^ - | cannot subsume the capability g* since that capability is not a SharedCapability + | cannot subsume the capability g* since that capability is not a `Sharable` capability | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/scoped-caps.scala:10:20 ---------------------------------- @@ -36,14 +36,14 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/scoped-caps.scala:13:25 ---------------------------------- 13 | val _: (x: A^) -> B^ = x => f(x) // error: existential in B cannot subsume `x` since `x` is not shared | ^^^^^^^^^ - | Found: (x: A^) ->? B^{x} - | Required: (x: A^) -> B^² + | Found: (x: A^) ->? B^{x} + | Required: (x: A^) -> B^² | - | where: ^ refers to the universal root capability - | ^² refers to a root capability associated with the result type of (x: A^): B^² + | where: ^ refers to the universal root capability + | ^² refers to a root capability associated with the result type of (x: A^): B^² | - | Note that the existential capture root in B^ - | cannot subsume the capability x.type since that capability is not a SharedCapability + | Note that the existential capture root in B^ + | cannot subsume the capability x.type since that capability is not a `Sharable` capability | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/scoped-caps.scala:16:24 ---------------------------------- @@ -56,7 +56,7 @@ | cap is the universal root capability | | Note that the existential capture root in B^ - | cannot subsume the capability h* since that capability is not a SharedCapability + | cannot subsume the capability h* since that capability is not a `Sharable` capability | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/scoped-caps.scala:17:24 ---------------------------------- @@ -69,7 +69,7 @@ | cap is the universal root capability | | Note that the existential capture root in B^ - | cannot subsume the capability h* since that capability is not a SharedCapability + | cannot subsume the capability h* since that capability is not a `Sharable` capability | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/scoped-caps.scala:26:19 ---------------------------------- diff --git a/tests/neg-custom-args/captures/scoped-caps.scala b/tests/neg-custom-args/captures/scoped-caps.scala index 184501d08288..9d11f9f00b7e 100644 --- a/tests/neg-custom-args/captures/scoped-caps.scala +++ b/tests/neg-custom-args/captures/scoped-caps.scala @@ -1,6 +1,6 @@ class A class B -class S extends caps.SharedCapability +class S extends caps.Sharable def test(io: Object^): Unit = val f: (x: A^) -> B^ = ??? diff --git a/tests/neg-custom-args/captures/shared-capability.scala b/tests/neg-custom-args/captures/shared-capability.scala index 262a6db386ba..f10bc6f53444 100644 --- a/tests/neg-custom-args/captures/shared-capability.scala +++ b/tests/neg-custom-args/captures/shared-capability.scala @@ -1,8 +1,8 @@ -import caps.SharedCapability +import caps.Sharable -class Async extends SharedCapability +class Async extends Sharable def test1(a: Async): Object^ = a // OK diff --git a/tests/pos-custom-args/captures/capt-capability.scala b/tests/pos-custom-args/captures/capt-capability.scala index d82f78263d18..21c63d3fa608 100644 --- a/tests/pos-custom-args/captures/capt-capability.scala +++ b/tests/pos-custom-args/captures/capt-capability.scala @@ -1,4 +1,4 @@ -import caps.{Capability, SharedCapability} +import caps.{Capability, Sharable} def f1(c: Capability): () ->{c} c.type = () => c // ok @@ -14,7 +14,7 @@ def f3: Int = x def foo() = - val x: SharedCapability = ??? + val x: Sharable = ??? val y: Capability = x val x2: () ->{x} Capability = ??? val y2: () ->{x} Capability = x2 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 c76e6067fbef..7d06edd36415 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, SharedCapability} +import caps.{CapSet, Sharable} import caps.use @experimental object Test: - class Async extends SharedCapability + class Async extends Sharable def listener(async: Async): Listener^{async} = ??? diff --git a/tests/pos-custom-args/captures/i16226.scala b/tests/pos-custom-args/captures/i16226.scala index af0a44e6bdfc..79893d7266ba 100644 --- a/tests/pos-custom-args/captures/i16226.scala +++ b/tests/pos-custom-args/captures/i16226.scala @@ -1,4 +1,4 @@ -class Cap extends caps.SharedCapability +class Cap extends caps.Sharable class LazyRef[T](val elem: () => T): val get: () ->{elem} T = elem diff --git a/tests/pos-custom-args/captures/reach-capability.scala b/tests/pos-custom-args/captures/reach-capability.scala index 7160b280ce4f..77bd91957fa0 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.SharedCapability +import caps.Sharable 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 SharedCapability + class Label extends Sharable class Listener diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index fd0281c5fffc..d26047f50cb8 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -38,7 +38,8 @@ val experimentalDefinitionInLibrary = Set( "scala.caps.Contains$.containsImpl", "scala.caps.Exists", "scala.caps.Mutable", - "scala.caps.SharedCapability", + "scala.caps.Sharable", + "scala.caps.Control", "scala.caps.consume", "scala.caps.internal", "scala.caps.internal$", From 7eae7e3bdf28558ca15714f8b323bf7e0ade676d Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 7 Jul 2025 16:50:57 +0200 Subject: [PATCH 3/7] Introduce only-capabilities Basic representations and definitions, so that we can parse only-capabilities, convert them to internal representation `Restrict(c, cls)`, not crash on them during capture checking and print them out correctly. --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 3 + .../src/dotty/tools/dotc/cc/Capability.scala | 74 +++++++++++++++---- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 14 +++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 19 ++++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 + .../src/dotty/tools/dotc/cc/SepCheck.scala | 1 + .../tools/dotc/config/SourceVersion.scala | 4 +- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../src/dotty/tools/dotc/core/Types.scala | 4 + .../dotty/tools/dotc/parsing/Parsers.scala | 27 +++++-- .../tools/dotc/printing/PlainPrinter.scala | 1 + docs/_docs/internals/syntax.md | 3 +- .../annotation/internal/onlyCapability.scala | 8 ++ project/MiMaFilters.scala | 1 + tests/new/test.scala | 18 +---- .../captures/restrict-try.scala | 13 ++++ 17 files changed, 149 insertions(+), 45 deletions(-) create mode 100644 library/src/scala/annotation/internal/onlyCapability.scala create mode 100644 tests/pos-custom-args/captures/restrict-try.scala diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 8006f1c510be..96c8c4c4f845 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -556,6 +556,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def makeReadOnlyAnnot()(using Context): Tree = New(ref(defn.ReadOnlyCapabilityAnnot.typeRef), Nil :: Nil) + def makeOnlyAnnot(qid: Tree)(using Context) = + New(AppliedTypeTree(ref(defn.OnlyCapabilityAnnot.typeRef), qid :: Nil), Nil :: Nil) + def makeConstructor(tparams: List[TypeDef], vparamss: List[List[ValDef]], rhs: Tree = EmptyTree)(using Context): DefDef = DefDef(nme.CONSTRUCTOR, joinParams(tparams, vparamss), TypeTree(), rhs) diff --git a/compiler/src/dotty/tools/dotc/cc/Capability.scala b/compiler/src/dotty/tools/dotc/cc/Capability.scala index 5c1de33aea0e..b01fee352826 100644 --- a/compiler/src/dotty/tools/dotc/cc/Capability.scala +++ b/compiler/src/dotty/tools/dotc/cc/Capability.scala @@ -39,8 +39,9 @@ import annotation.internal.sharable * | +-- SetCapability -----+-- TypeRef * | +-- TypeParamRef * | - * +-- DerivedCapability -+-- ReadOnly - * +-- Reach + * +-- DerivedCapability -+-- Reach + * +-- Only + * +-- ReadOnly * +-- Maybe * * All CoreCapabilities are Types, or, more specifically instances of TypeProxy. @@ -96,9 +97,18 @@ object Capabilities: * but they can wrap reach capabilities. We have * (x?).readOnly = (x.rd)? */ - case class ReadOnly(underlying: ObjectCapability | RootCapability | Reach) - extends DerivedCapability: - assert(!underlying.isInstanceOf[Maybe]) + case class ReadOnly(underlying: ObjectCapability | RootCapability | Reach | Restricted) + extends DerivedCapability + + /** The restricted capability `x.only[C]`. We have {x.only[C]} <: {x}. + * + * Restricted capabilities cannot wrap maybe capabilities or read-only capabilities + * but they can wrap reach capabilities. We have + * (x?).restrict[T] = (x.restrict[T])? + * (x.rd).restrict[T] = (x.restrict[T]).rd + */ + case class Restricted(underlying: ObjectCapability | RootCapability | Reach, cls: ClassSymbol) + extends DerivedCapability /** If `x` is a capability, its reach capability `x*`. `x*` stands for all * capabilities reachable through `x`. @@ -109,11 +119,11 @@ object Capabilities: * * Reach capabilities cannot wrap read-only capabilities or maybe capabilities. * We have - * (x.rd).reach = x*.rd - * (x.rd)? = (x*)? + * (x?).reach = (x.reach)? + * (x.rd).reach = (x.reach).rd + * (x.only[T]).reach = (x*).only[T] */ - case class Reach(underlying: ObjectCapability) extends DerivedCapability: - assert(!underlying.isInstanceOf[Maybe | ReadOnly]) + case class Reach(underlying: ObjectCapability) extends DerivedCapability /** The global root capability referenced as `caps.cap` * `cap` does not subsume other capabilities, except in arguments of @@ -124,6 +134,7 @@ object Capabilities: def descr(using Context) = "the universal root capability" override val maybe = Maybe(this) override val readOnly = ReadOnly(this) + override def restrict(cls: ClassSymbol)(using Context) = Restricted(this, cls) override def reach = unsupported("cap.reach") override def singletonCaptureSet(using Context) = CaptureSet.universal override def captureSetOfInfo(using Context) = singletonCaptureSet @@ -242,7 +253,7 @@ object Capabilities: /** 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 order: - * `*` first, `.rd` next, `?` last. + * `*` first, `.only` next, `.rd` next, `?` last. */ trait Capability extends Showable: @@ -254,7 +265,15 @@ object Capabilities: protected def cached[C <: DerivedCapability](newRef: C): C = def recur(refs: List[DerivedCapability]): C = refs match case ref :: refs1 => - if ref.getClass == newRef.getClass then ref.asInstanceOf[C] else recur(refs1) + val exists = ref match + case Restricted(_, cls) => + newRef match + case Restricted(_, newCls) => cls == newCls + case _ => false + case _ => + ref.getClass == newRef.getClass + if exists then ref.asInstanceOf[C] + else recur(refs1) case Nil => myDerived = newRef :: myDerived newRef @@ -267,11 +286,24 @@ object Capabilities: def readOnly: ReadOnly | Maybe = this match case Maybe(ref1) => Maybe(ref1.readOnly) case self: ReadOnly => self - case self: (ObjectCapability | RootCapability | Reach) => cached(ReadOnly(self)) - - def reach: Reach | ReadOnly | Maybe = this match + case self: (ObjectCapability | RootCapability | Reach | Restricted) => cached(ReadOnly(self)) + + def restrict(cls: ClassSymbol)(using Context): Restricted | ReadOnly | Maybe = this match + case Maybe(ref1) => Maybe(ref1.restrict(cls)) + case ReadOnly(ref1) => ReadOnly(ref1.restrict(cls).asInstanceOf[Restricted]) + case self @ Restricted(ref1, prevCls) => + val combinedCls = + if prevCls.isSubClass(cls) then prevCls + else if cls.isSubClass(prevCls) then cls + else defn.NothingClass + if combinedCls == prevCls then self + else cached(Restricted(ref1, combinedCls)) + case self: (ObjectCapability | RootCapability | Reach) => cached(Restricted(self, cls)) + + def reach: Reach | Restricted | ReadOnly | Maybe = this match case Maybe(ref1) => Maybe(ref1.reach) - case ReadOnly(ref1) => ReadOnly(ref1.reach.asInstanceOf[Reach]) + case ReadOnly(ref1) => ReadOnly(ref1.reach.asInstanceOf[Reach | Restricted]) + case Restricted(ref1, cls) => Restricted(ref1.reach.asInstanceOf[Reach], cls) case self: Reach => self case self: ObjectCapability => cached(Reach(self)) @@ -285,6 +317,12 @@ object Capabilities: case tp: SetCapability => tp.captureSetOfInfo.isReadOnly case _ => this ne stripReadOnly + final def restriction(using Context): Symbol = this match + case Restricted(_, cls) => cls + case ReadOnly(ref1) => ref1.restriction + case Maybe(ref1) => ref1.restriction + case _ => NoSymbol + /** Is this a reach reference of the form `x*` or a readOnly or maybe variant * of a reach reference? */ @@ -299,6 +337,12 @@ object Capabilities: case Maybe(ref1) => ref1.stripReadOnly.maybe case _ => this + final def stripRestricted(using Context): Capability = this match + case Restricted(ref1, _) => ref1 + case ReadOnly(ref1) => ref1.stripRestricted.readOnly + case Maybe(ref1) => ref1.stripRestricted.maybe + case _ => this + final def stripReach(using Context): Capability = this match case Reach(ref1) => ref1 case ReadOnly(ref1) => ref1.stripReach.readOnly diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 2f5c59c11071..cdafb827369b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -80,6 +80,8 @@ extension (tp: Type) tp1.toCapability.reach case ReadOnlyCapability(tp1) => tp1.toCapability.readOnly + case OnlyCapability(tp1, cls) => + tp1.toCapability.restrict(cls) // for now case ref: TermRef if ref.isCapRef => GlobalCap case ref: Capability if ref.isTrackableRef => @@ -587,7 +589,6 @@ abstract class AnnotatedCapability(annotCls: Context ?=> ClassSymbol): def unapply(tree: AnnotatedType)(using Context): Option[Type] = tree match case AnnotatedType(parent: Type, ann) if ann.hasSymbol(annotCls) => Some(parent) case _ => None - end AnnotatedCapability /** An extractor for `ref @readOnlyCapability`, which is used to express @@ -605,6 +606,17 @@ object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot) */ object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot) +object OnlyCapability: + def apply(tp: Type, cls: ClassSymbol)(using Context): AnnotatedType = + AnnotatedType(tp, + Annotation(defn.OnlyCapabilityAnnot.typeRef.appliedTo(cls.typeRef), Nil, util.Spans.NoSpan)) + + def unapply(tree: AnnotatedType)(using Context): Option[(Type, ClassSymbol)] = tree match + case AnnotatedType(parent: Type, ann) if ann.hasSymbol(defn.OnlyCapabilityAnnot) => + Some((parent, ann.tree.tpe.argTypes.head.classSymbol.asClass)) + case _ => None +end OnlyCapability + /** 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 37ee3f68b9ad..dda4cc8a4608 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -403,6 +403,8 @@ sealed abstract class CaptureSet extends Showable: def maybe(using Context): CaptureSet = map(MaybeMap()) + def restrict(cls: ClassSymbol)(using Context): CaptureSet = map(RestrictMap(cls)) + def readOnly(using Context): CaptureSet = val res = map(ReadOnlyMap()) if mutability != Ignored then res.mutability = Reader @@ -1344,9 +1346,10 @@ 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 apply(t: Type) = mapOver(t) + protected def isSameMap(other: BiTypeMap) = other.getClass == getClass + override def fuse(next: BiTypeMap)(using Context) = next match case next: Inverse if next.inverse.getClass == getClass => Some(IdentityTypeMap) case next: NarrowingCapabilityMap if next.getClass == getClass => Some(this) @@ -1358,8 +1361,8 @@ object CaptureSet: def inverse = NarrowingCapabilityMap.this override def toString = NarrowingCapabilityMap.this.toString ++ ".inverse" override def fuse(next: BiTypeMap)(using Context) = next match - case next: NarrowingCapabilityMap if next.inverse.getClass == getClass => Some(IdentityTypeMap) - case next: NarrowingCapabilityMap if next.getClass == getClass => Some(this) + case next: NarrowingCapabilityMap if isSameMap(next.inverse) => Some(IdentityTypeMap) + case next: NarrowingCapabilityMap if isSameMap(next) => Some(this) case _ => None lazy val inverse = Inverse() @@ -1375,6 +1378,13 @@ object CaptureSet: override def mapCapability(c: Capability, deep: Boolean) = c.readOnly override def toString = "ReadOnly" + private class RestrictMap(val cls: ClassSymbol)(using Context) extends NarrowingCapabilityMap: + override def mapCapability(c: Capability, deep: Boolean) = c.restrict(cls) + override def toString = "Restrict" + override def isSameMap(other: BiTypeMap) = other match + case other: RestrictMap => cls == other.cls + case _ => false + /* Not needed: def ofClass(cinfo: ClassInfo, argTypes: List[Type])(using Context): CaptureSet = CaptureSet.empty @@ -1402,6 +1412,9 @@ object CaptureSet: case Reach(c1) => c1.widen.deepCaptureSet(includeTypevars = true) .showing(i"Deep capture set of $c: ${c1.widen} = ${result}", capt) + case Restricted(c1, cls) => + if cls == defn.NothingClass then CaptureSet.empty + else c1.captureSetOfInfo.restrict(cls) // todo: should we simplify using subsumption here? case ReadOnly(c1) => c1.captureSetOfInfo.readOnly case Maybe(c1) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 1275c1b38eb6..74646da5d453 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -112,6 +112,8 @@ object CheckCaptures: report.error(em"Cannot form a reach capability from `cap`", ann.srcPos) case ReadOnlyCapability(ref) => check(ref) + case OnlyCapability(ref, cls) => + check(ref) case tpe => report.error(em"$elem: $tpe is not a legal element of a capture set", ann.srcPos) ann.retainedSet.retainedElementsRaw.foreach(check) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index f94a8172cd11..cef3bed3b703 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -256,6 +256,7 @@ object SepCheck: def hiddenByElem(elem: Capability): Refs = elem match case elem: FreshCap => elem.hiddenSet.elems ++ recur(elem.hiddenSet.elems) + case Restricted(elem1, cls) => hiddenByElem(elem1).map(_.restrict(cls)) case ReadOnly(elem1) => hiddenByElem(elem1).map(_.readOnly) case _ => emptyRefs diff --git a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala index d662d3c0d412..cf5610dcdbf2 100644 --- a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala +++ b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala @@ -8,7 +8,7 @@ import Feature.isPreviewEnabled import util.Property enum SourceVersion: - case `3.0-migration`, `3.0` + case `3.0-migration`, `3.0` case `3.1-migration`, `3.1` case `3.2-migration`, `3.2` case `3.3-migration`, `3.3` @@ -45,7 +45,7 @@ enum SourceVersion: def enablesBetterFors(using Context) = isAtLeast(`3.7`) && isPreviewEnabled object SourceVersion extends Property.Key[SourceVersion]: - + /* The default source version used by the built compiler */ val defaultSourceVersion = `3.7` diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e70e5690f32f..5716b1230e15 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1089,6 +1089,7 @@ class Definitions { @tu lazy val ReachCapabilityAnnot = requiredClass("scala.annotation.internal.reachCapability") @tu lazy val RootCapabilityAnnot = requiredClass("scala.caps.internal.rootCapability") @tu lazy val ReadOnlyCapabilityAnnot = requiredClass("scala.annotation.internal.readOnlyCapability") + @tu lazy val OnlyCapabilityAnnot = requiredClass("scala.annotation.internal.onlyCapability") @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @tu lazy val RetainsCapAnnot: ClassSymbol = requiredClass("scala.annotation.retainsCap") diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 5c4b8629e43a..9271961d02dd 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -568,6 +568,7 @@ object StdNames { val null_ : N = "null" val ofDim: N = "ofDim" val on: N = "on" + val only: N = "only" val opaque: N = "opaque" val open: N = "open" val ordinal: N = "ordinal" diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 61b3b958fca3..d4b068ae41c1 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6278,6 +6278,10 @@ object Types extends TypeUtils { case c: RootCapability => c case Reach(c1) => mapCapability(c1, deep = true) + case Restricted(c1, cls) => + mapCapability(c1) match + case c2: Capability => c2.restrict(cls) + case (cs: CaptureSet, exact) => (cs.restrict(cls), exact) case ReadOnly(c1) => assert(!deep) mapCapability(c1) match diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index da5a59c52661..52903077ec3d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1589,24 +1589,35 @@ object Parsers { case _ => None } - /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [`.` `rd`] -- under captureChecking + /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [CapFilter] [`.` `rd`] -- under captureChecking + * CapFilter ::= `.` `as` `[` QualId `]` */ def captureRef(): Tree = - def derived(ref: Tree, ann: () => Tree) = - in.nextToken() - atSpan(startOffset(ref)) { Annotated(ref, ann()) } + def derived(ref: Tree): Tree = + atSpan(startOffset(ref)): + if in.isIdent(nme.raw.STAR) then + in.nextToken() + Annotated(ref, makeReachAnnot()) + else if in.isIdent(nme.rd) then + in.nextToken() + Annotated(ref, makeReadOnlyAnnot()) + else if in.isIdent(nme.only) then + in.nextToken() + Annotated(ref, makeOnlyAnnot(inBrackets(convertToTypeId(qualId())))) + else assert(false) def recur(ref: Tree): Tree = if in.token == DOT then in.nextToken() - if in.isIdent(nme.rd) then derived(ref, makeReadOnlyAnnot) + if in.isIdent(nme.rd) || in.isIdent(nme.only) then derived(ref) else recur(selector(ref)) else if in.isIdent(nme.raw.STAR) then - val reachRef = derived(ref, makeReachAnnot) - if in.token == DOT && in.lookahead.isIdent(nme.rd) then + val reachRef = derived(ref) + val next = in.lookahead + if in.token == DOT && (next.isIdent(nme.rd) || next.isIdent(nme.only)) then in.nextToken() - derived(reachRef, makeReadOnlyAnnot) + derived(reachRef) else reachRef else ref diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index f6e60cd990d6..41f8d95a3ebc 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -467,6 +467,7 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextCapability(c: Capability): Text = c match case ReadOnly(c1) => toTextCapability(c1) ~ ".rd" + case Restricted(c1, cls) => toTextCapability(c1) ~ s".only[${nameString(cls)}]" case Reach(c1) => toTextCapability(c1) ~ "*" case Maybe(c1) => toTextCapability(c1) ~ "?" case GlobalCap => "cap" diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 899b7f5d3c0b..9a5d3d4b2776 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -235,7 +235,8 @@ TypeBound ::= Type NamesAndTypes ::= NameAndType {‘,’ NameAndType} NameAndType ::= id ':' Type CaptureSet ::= ‘{’ CaptureRef {‘,’ CaptureRef} ‘}’ -- under captureChecking -CaptureRef ::= { SimpleRef ‘.’ } SimpleRef [‘*’] [‘.’ ‘rd’] -- under captureChecking +CaptureRef ::= { SimpleRef ‘.’ } SimpleRef [‘*’] [CapFilter] [‘.’ ‘rd’] -- under captureChecking +CapFilter ::= ‘.’ ‘as’ ‘[’ QualId ’]’ -- under captureChecking ``` ### Expressions diff --git a/library/src/scala/annotation/internal/onlyCapability.scala b/library/src/scala/annotation/internal/onlyCapability.scala new file mode 100644 index 000000000000..6eaa72d45dcc --- /dev/null +++ b/library/src/scala/annotation/internal/onlyCapability.scala @@ -0,0 +1,8 @@ +package scala.annotation +package internal + +/** An annotation that represents a capability `c.only[T]`, + * encoded as `x.type @onlyCapability[T]` + */ +class onlyCapability[T] extends StaticAnnotation + diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index c57136b262dd..cbfc1affa332 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -9,6 +9,7 @@ object MiMaFilters { // Additions that require a new minor version of the library Build.mimaPreviousDottyVersion -> Seq( ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.readOnlyCapability"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.asCapability"), // Scala.js-only class ProblemFilters.exclude[FinalClassProblem]("scala.scalajs.runtime.AnonFunctionXXL"), diff --git a/tests/new/test.scala b/tests/new/test.scala index d350e15a8c9f..5a107ed2b5d1 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,15 +1,3 @@ - -package foo - -package object bar: - opaque type O[X] >: X = X - -class Test: - import bar.O - - val x = "abc" - val y: O[String] = x - //val z: String = y - - - +type C^ = {caps.cap} +type D^ >: {caps.cap} <: C +type E^ >: D <: C diff --git a/tests/pos-custom-args/captures/restrict-try.scala b/tests/pos-custom-args/captures/restrict-try.scala new file mode 100644 index 000000000000..eb7a8bca87ed --- /dev/null +++ b/tests/pos-custom-args/captures/restrict-try.scala @@ -0,0 +1,13 @@ +import caps.Capability + +class Try[+T] +case class Ok[T](x: T) extends Try[T] +case class Fail(ex: Exception) extends Try[Nothing] + +import util.Try +class Control extends Capability + +object Try: + def apply[T](body: => T): Try[T]^{body.only[Control]} = + try Ok(body) + catch case ex: Exception => Fail(ex) From f8458554175a0c491ee22e2235199be2d57596bc Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 7 Jul 2025 18:18:36 +0200 Subject: [PATCH 4/7] Add showRef utility method --- compiler/src/dotty/tools/dotc/core/TypeUtils.scala | 4 ++++ compiler/src/dotty/tools/dotc/typer/Implicits.scala | 6 +++--- compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 82c027744c38..1b0f87f8f6b1 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -267,5 +267,9 @@ class TypeUtils: self.decl(nme.CONSTRUCTOR).altsWith(isApplicable).map(_.symbol) + def showRef(using Context): String = self match + case self: SingletonType => ctx.printer.toTextRef(self).show + case _ => self.show + end TypeUtils diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 25fed4e62de9..3952959c93e4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -558,8 +558,8 @@ object Implicits: var str1 = err.refStr(alt1.ref) var str2 = err.refStr(alt2.ref) if str1 == str2 then - str1 = ctx.printer.toTextRef(alt1.ref).show - str2 = ctx.printer.toTextRef(alt2.ref).show + str1 = alt1.ref.showRef + str2 = alt2.ref.showRef em"both $str1 and $str2 $qualify".withoutDisambiguation() override def toAdd(using Context) = @@ -1724,7 +1724,7 @@ trait Implicits: "argument" def showResult(r: SearchResult) = r match - case r: SearchSuccess => ctx.printer.toTextRef(r.ref).show + case r: SearchSuccess => r.ref.showRef case r => r.show result match diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 98fbede5f5ba..f9027cf7a961 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -336,7 +336,7 @@ trait ImportSuggestions: if ref.symbol.is(ExtensionMethod) then s"${ctx.printer.toTextPrefixOf(ref).show}${ref.symbol.name}" else - ctx.printer.toTextRef(ref).show + ref.showRef s" import $imported" val suggestions = suggestedRefs .zip(suggestedRefs.map(importString)) From e1965f6a39ab6a5082bd96ada4fb94c5befbde84 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 11 Jul 2025 10:29:45 +0200 Subject: [PATCH 5/7] Introduce capability classifiers --- .../src/dotty/tools/dotc/cc/Capability.scala | 106 ++++++++++++++++-- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 58 ++++++---- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 36 +++++- .../dotty/tools/dotc/cc/CapturingType.scala | 2 + .../dotty/tools/dotc/cc/CheckCaptures.scala | 5 + .../src/dotty/tools/dotc/cc/SepCheck.scala | 6 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 15 ++- .../src/dotty/tools/dotc/core/Contexts.scala | 7 ++ .../dotty/tools/dotc/core/Definitions.scala | 6 +- .../dotty/tools/dotc/core/TypeComparer.scala | 10 +- .../tools/dotc/printing/PlainPrinter.scala | 5 +- library/src/scala/caps/package.scala | 13 ++- .../captures/classified-inheritance.check | 8 ++ .../captures/classified-inheritance.scala | 10 ++ .../captures/classified-wf.check | 5 + .../captures/classified-wf.scala | 8 ++ .../captures/classifiers-1.scala | 9 ++ .../captures/shared-capability.check | 2 +- .../captures/restrict-try.scala | 5 +- .../stdlibExperimentalDefinitions.scala | 1 + 20 files changed, 262 insertions(+), 55 deletions(-) create mode 100644 tests/neg-custom-args/captures/classified-inheritance.check create mode 100644 tests/neg-custom-args/captures/classified-inheritance.scala create mode 100644 tests/neg-custom-args/captures/classified-wf.check create mode 100644 tests/neg-custom-args/captures/classified-wf.scala create mode 100644 tests/neg-custom-args/captures/classifiers-1.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Capability.scala b/compiler/src/dotty/tools/dotc/cc/Capability.scala index b01fee352826..de28f62f42a2 100644 --- a/compiler/src/dotty/tools/dotc/cc/Capability.scala +++ b/compiler/src/dotty/tools/dotc/cc/Capability.scala @@ -258,9 +258,11 @@ object Capabilities: trait Capability extends Showable: private var myCaptureSet: CaptureSet | Null = uninitialized - private var myCaptureSetValid: Validity = invalid + private var captureSetValid: Validity = invalid private var mySingletonCaptureSet: CaptureSet.Const | Null = null private var myDerived: List[DerivedCapability] = Nil + private var myClassifiers: Classifiers = UnknownClassifier + private var classifiersValid: Validity = invalid protected def cached[C <: DerivedCapability](newRef: C): C = def recur(refs: List[DerivedCapability]): C = refs match @@ -292,10 +294,7 @@ object Capabilities: case Maybe(ref1) => Maybe(ref1.restrict(cls)) case ReadOnly(ref1) => ReadOnly(ref1.restrict(cls).asInstanceOf[Restricted]) case self @ Restricted(ref1, prevCls) => - val combinedCls = - if prevCls.isSubClass(cls) then prevCls - else if cls.isSubClass(prevCls) then cls - else defn.NothingClass + val combinedCls = leastClassifier(prevCls, cls) if combinedCls == prevCls then self else cached(Restricted(ref1, combinedCls)) case self: (ObjectCapability | RootCapability | Reach) => cached(Restricted(self, cls)) @@ -469,7 +468,7 @@ object Capabilities: 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) + def derivesFromSharable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Sharable) /** The capture set consisting of exactly this reference */ def singletonCaptureSet(using Context): CaptureSet.Const = @@ -479,7 +478,7 @@ object Capabilities: /** The capture set of the type underlying this reference */ def captureSetOfInfo(using Context): CaptureSet = - if myCaptureSetValid == currentId then myCaptureSet.nn + if captureSetValid == currentId then myCaptureSet.nn else if myCaptureSet.asInstanceOf[AnyRef] eq CaptureSet.Pending then CaptureSet.empty else myCaptureSet = CaptureSet.Pending @@ -491,11 +490,60 @@ object Capabilities: myCaptureSet = null else myCaptureSet = computed - myCaptureSetValid = currentId + captureSetValid = currentId computed + /** The transitive classifiers of this capability. */ + def transClassifiers(using Context): Classifiers = + def toClassifiers(cls: ClassSymbol): Classifiers = + if cls == defn.AnyClass then Unclassified + else ClassifiedAs(cls :: Nil) + if classifiersValid != currentId then + myClassifiers = this match + case self: FreshCap => + toClassifiers(self.hiddenSet.classifier) + case self: RootCapability => + Unclassified + case Restricted(_, cls) => + assert(cls != defn.AnyClass) + if cls == defn.NothingClass then ClassifiedAs(Nil) + else ClassifiedAs(cls :: Nil) + case ReadOnly(ref1) => + ref1.transClassifiers + case Maybe(ref1) => + ref1.transClassifiers + case Reach(_) => + captureSetOfInfo.transClassifiers + case self: CoreCapability => + joinClassifiers(toClassifiers(self.classifier), captureSetOfInfo.transClassifiers) + if myClassifiers != UnknownClassifier then + classifiersValid == currentId + myClassifiers + end transClassifiers + + def tryClassifyAs(cls: ClassSymbol)(using Context): Boolean = + cls == defn.AnyClass + || this.match + case self: FreshCap => + self.hiddenSet.tryClassifyAs(cls) + case self: RootCapability => + true + case Restricted(_, cls1) => + assert(cls != defn.AnyClass) + cls1.isSubClass(cls) + case ReadOnly(ref1) => + ref1.tryClassifyAs(cls) + case Maybe(ref1) => + ref1.tryClassifyAs(cls) + case Reach(_) => + captureSetOfInfo.tryClassifyAs(cls) + case self: CoreCapability => + self.classifier.isSubClass(cls) + && captureSetOfInfo.tryClassifyAs(cls) + def invalidateCaches() = - myCaptureSetValid = invalid + captureSetValid = invalid + classifiersValid = invalid /** x subsumes x * x =:= y ==> x subsumes y @@ -603,12 +651,15 @@ object Capabilities: vs.ifNotSeen(this)(x.hiddenSet.elems.exists(_.subsumes(y))) || levelOK + && ( y.tryClassifyAs(x.hiddenSet.classifier) + || { capt.println(i"$y is not classified as $x"); false } + ) && canAddHidden && vs.addHidden(x.hiddenSet, y) case x: ResultCap => val result = y match case y: ResultCap => vs.unify(x, y) - case _ => y.derivesFromSharedCapability + case _ => y.derivesFromSharable if !result then TypeComparer.addErrorNote(CaptureSet.ExistentialSubsumesFailure(x, y)) result @@ -618,7 +669,7 @@ object Capabilities: case _: ResultCap => false case _: FreshCap if CCState.collapseFresh => true case _ => - y.derivesFromSharedCapability + y.derivesFromSharable || canAddHidden && vs != VarState.HardSeparate && CCState.capIsRoot case _ => y match @@ -674,6 +725,39 @@ object Capabilities: def toText(printer: Printer): Text = printer.toTextCapability(this) end Capability + /** Result type of `transClassifiers`. Interprete as follows: + * UnknownClassifier: No list could be computed since some capture sets + * are still unsolved variables + * Unclassified : No set exists since some parts of tcs are not classified + * ClassifiedAs(clss: All parts of tcss are classified with classes in clss + */ + enum Classifiers: + case UnknownClassifier + case Unclassified + case ClassifiedAs(clss: List[ClassSymbol]) + + export Classifiers.{UnknownClassifier, Unclassified, ClassifiedAs} + + /** The least classifier between `cls1` and `cls2`, which are either + * AnyClass, NothingClass, or a class directly extending caps.Classifier. + * @return if oen of cls1, cls2 is a subclass of the other, the subclass + * otherwise NothingClass (which is a subclass of all classes) + */ + def leastClassifier(cls1: ClassSymbol, cls2: ClassSymbol)(using Context): ClassSymbol = + if cls1.isSubClass(cls2) then cls1 + else if cls2.isSubClass(cls1) then cls2 + else defn.NothingClass + + def joinClassifiers(cs1: Classifiers, cs2: Classifiers)(using Context): Classifiers = + // Drop classes that subclass classes of the other set + def filterSub(cs1: List[ClassSymbol], cs2: List[ClassSymbol]) = + cs1.filter(cls1 => !cs2.exists(cls2 => cls1.isSubClass(cls2))) + (cs1, cs2) match + case (Unclassified, _) | (_, Unclassified) => Unclassified + case (UnknownClassifier, _) | (_, UnknownClassifier) => UnknownClassifier + case (ClassifiedAs(cs1), ClassifiedAs(cs2)) => + ClassifiedAs(filterSub(cs1, cs2) ++ filterSub(cs2, cs1)) + /** The place of - and cause for - creating a fresh capability. Used for * error diagnostics */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index cdafb827369b..a096f9696f32 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -376,7 +376,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) + def derivesFromSharedCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Sharable) /** Drop @retains annotations everywhere */ def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling @@ -442,6 +442,30 @@ extension (tp: Type) def dropUseAndConsumeAnnots(using Context): Type = tp.dropAnnot(defn.UseAnnot).dropAnnot(defn.ConsumeAnnot) + /** If `tp` is a function or method, a type of the same kind with the given + * argument and result types. + */ + def derivedFunctionOrMethod(argTypes: List[Type], resType: Type)(using Context): Type = tp match + case tp @ AppliedType(tycon, args) if defn.isNonRefinedFunction(tp) => + val args1 = argTypes :+ resType + if args.corresponds(args1)(_ eq _) then tp + else tp.derivedAppliedType(tycon, args1) + case tp @ defn.RefinedFunctionOf(rinfo) => + val rinfo1 = rinfo.derivedFunctionOrMethod(argTypes, resType) + if rinfo1 eq rinfo then tp + else if rinfo1.isInstanceOf[PolyType] then tp.derivedRefinedType(refinedInfo = rinfo1) + else rinfo1.toFunctionType(alwaysDependent = true) + case tp: MethodType => + tp.derivedLambdaType(paramInfos = argTypes, resType = resType) + case tp: PolyType => + assert(argTypes.isEmpty) + tp.derivedLambdaType(resType = resType) + case _ => + tp + + def classifier(using Context): ClassSymbol = + tp.classSymbols.map(_.classifier).foldLeft(defn.AnyClass)(leastClassifier) + extension (tp: MethodType) /** A method marks an existential scope unless it is the prefix of a curried method */ def marksExistentialScope(using Context): Boolean = @@ -473,6 +497,16 @@ extension (cls: ClassSymbol) val selfType = bc.givenSelfType bc.is(CaptureChecked) && selfType.exists && selfType.captureSet.elems == refs.elems + def isClassifiedCapabilityClass(using Context): Boolean = + cls.derivesFrom(defn.Caps_Capability) && cls.parentSyms.contains(defn.Caps_Classifier) + + def classifier(using Context): ClassSymbol = + if cls.derivesFrom(defn.Caps_Capability) then + cls.baseClasses + .filter(_.parentSyms.contains(defn.Caps_Classifier)) + .foldLeft(defn.AnyClass)(leastClassifier) + else defn.AnyClass + extension (sym: Symbol) /** This symbol is one of `retains` or `retainsCap` */ @@ -630,28 +664,6 @@ object FunctionOrMethod: case defn.RefinedFunctionOf(rinfo) => unapply(rinfo) case _ => None -/** If `tp` is a function or method, a type of the same kind with the given - * argument and result types. - */ -extension (self: Type) - def derivedFunctionOrMethod(argTypes: List[Type], resType: Type)(using Context): Type = self match - case self @ AppliedType(tycon, args) if defn.isNonRefinedFunction(self) => - val args1 = argTypes :+ resType - if args.corresponds(args1)(_ eq _) then self - else self.derivedAppliedType(tycon, args1) - case self @ defn.RefinedFunctionOf(rinfo) => - val rinfo1 = rinfo.derivedFunctionOrMethod(argTypes, resType) - if rinfo1 eq rinfo then self - else if rinfo1.isInstanceOf[PolyType] then self.derivedRefinedType(refinedInfo = rinfo1) - else rinfo1.toFunctionType(alwaysDependent = true) - case self: MethodType => - self.derivedLambdaType(paramInfos = argTypes, resType = resType) - case self: PolyType => - assert(argTypes.isEmpty) - self.derivedLambdaType(resType = resType) - case _ => - self - /** An extractor for a contains argument */ object ContainsImpl: def unapply(tree: TypeApply)(using Context): Option[(Tree, Tree)] = diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index dda4cc8a4608..5dd16e1bdb57 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -410,6 +410,20 @@ sealed abstract class CaptureSet extends Showable: if mutability != Ignored then res.mutability = Reader res + def transClassifiers(using Context): Classifiers = + if isConst then + (ClassifiedAs(Nil) /: elems.map(_.transClassifiers))(joinClassifiers) + else UnknownClassifier + + def tryClassifyAs(cls: ClassSymbol)(using Context): Boolean = + elems.forall(_.tryClassifyAs(cls)) + + def adoptClassifier(cls: ClassSymbol)(using Context): Unit = + for elem <- elems do + elem.stripReadOnly match + case fresh: FreshCap => fresh.hiddenSet.adoptClassifier(cls) + case _ => + /** A bad root `elem` is inadmissible as a member of this set. What is a bad roots depends * on the value of `rootLimit`. * If the limit is null, all capture roots are good. @@ -651,6 +665,25 @@ object CaptureSet: */ private[CaptureSet] var rootLimit: Symbol | Null = null + private var myClassifier: ClassSymbol = defn.AnyClass + def classifier: ClassSymbol = myClassifier + + private def narrowClassifier(cls: ClassSymbol)(using Context): Unit = + val newClassifier = leastClassifier(classifier, cls) + if newClassifier == defn.NothingClass then + println(i"conflicting classifications for $this, was $classifier, now $cls") + myClassifier = newClassifier + + override def adoptClassifier(cls: ClassSymbol)(using Context): Unit = + if !classifier.isSubClass(cls) then // serves as recursion brake + narrowClassifier(cls) + super.adoptClassifier(cls) + + override def tryClassifyAs(cls: ClassSymbol)(using Context): Boolean = + classifier.isSubClass(cls) + || super.tryClassifyAs(cls) + && { narrowClassifier(cls); true } + /** A handler to be invoked when new elems are added to this set */ var newElemAddedHandler: Capability => Context ?=> Unit = _ => () @@ -682,6 +715,8 @@ object CaptureSet: addIfHiddenOrFail(elem) else if !levelOK(elem) then failWith(IncludeFailure(this, elem, levelError = true)) // or `elem` is not visible at the level of the set. + else if !elem.tryClassifyAs(classifier) then + failWith(IncludeFailure(this, elem)) else // id == 108 then assert(false, i"trying to add $elem to $this") assert(elem.isWellformed, elem) @@ -689,7 +724,6 @@ object CaptureSet: includeElem(elem) if isBadRoot(rootLimit, elem) then rootAddedHandler() - newElemAddedHandler(elem) val normElem = if isMaybeSet then elem else elem.stripMaybe // assert(id != 5 || elems.size != 3, this) val res = deps.forall: dep => diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index 58bee8132b98..1cafe295f529 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -40,6 +40,8 @@ object CapturingType: apply(parent1, refs ++ refs1, boxed) case _ => if parent.derivesFromMutable then refs.setMutable() + val classifier = parent.classifier + refs.adoptClassifier(parent.classifier) AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) /** An extractor for CapturingTypes. Capturing types are recognized if diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 74646da5d453..615b3f41ef44 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -113,6 +113,11 @@ object CheckCaptures: case ReadOnlyCapability(ref) => check(ref) case OnlyCapability(ref, cls) => + if !cls.isClassifiedCapabilityClass then + report.error( + em"""${ref.showRef}.only[${cls.name}] is not well-formed since $cls is not a classifier class. + |A classifier class is a class extending `caps.Capability` and directly extending `caps.Classifier`.""", + ann.srcPos) check(ref) case tpe => report.error(em"$elem: $tpe is not a legal element of a capture set", ann.srcPos) diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index cef3bed3b703..2662a5481f83 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -598,7 +598,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: * - If the reference is to a this type of the enclosing class, the * access must be in a @consume method. * - * References that extend cpas.Sharable are excluded from checking. + * References that extend caps.Sharable are excluded from checking. * As a side effect, add all checked references with the given position `pos` * to the global `consumed` map. * @@ -612,7 +612,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: val badParams = mutable.ListBuffer[Symbol]() def currentOwner = role.dclSym.orElse(ctx.owner) for hiddenRef <- refsToCheck.deductSymRefs(role.dclSym).deduct(explicitRefs(tpe)) do - if !hiddenRef.derivesFromSharedCapability then + if !hiddenRef.derivesFromSharable then hiddenRef.pathRoot match case ref: TermRef => val refSym = ref.symbol @@ -649,7 +649,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: role match case _: TypeRole.Argument | _: TypeRole.Qualifier => for ref <- refsToCheck do - if !ref.derivesFromSharedCapability then + if !ref.derivesFromSharable 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 f686ac60298a..326c9567e3d0 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -393,7 +393,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // will be ignored anyway. fail(em"$parent is a pure type, it makes no sense to add a capture set to it") else if refs.isUniversal && parent.derivesFromSharedCapability then - fail(em"$tp extends SharedCapability, so it cannot capture `cap`") + fail(em"$tp extends Sharable, so it cannot capture `cap`") case _ => tp @@ -696,6 +696,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree: TypeDef => tree.symbol match case cls: ClassSymbol => + checkClassifiedInheritance(cls) ccState.inNestedLevelUnless(cls.is(Module)): val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo @@ -912,6 +913,18 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def setupUnit(tree: Tree, checker: CheckerAPI)(using Context): Unit = setupTraverser(checker).traverse(tree)(using ctx.withPhase(thisPhase)) + // ------ Checks to run at Setup ---------------------------------------- + + private def checkClassifiedInheritance(cls: ClassSymbol)(using Context): Unit = + def recur(cs: List[ClassSymbol]): Unit = cs match + case c :: cs1 => + for c1 <- cs1 do + if !c.derivesFrom(c1) && !c1.derivesFrom(c) then + report.error(i"$cls inherits two unrelated classifier traits: $c and $c1", cls.srcPos) + recur(cs1) + case Nil => + recur(cls.baseClasses.filter(_.isClassifiedCapabilityClass).distinct) + // ------ Checks to run after main capture checking -------------------------- /** A list of actions to perform at postCheck */ diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 85edadd40c80..9de714be8c37 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -870,6 +870,13 @@ object Contexts { result.init(ctx) result + def currentComparer(using Context): TypeComparer = + val base = ctx.base + if base.comparersInUse > 0 then + base.comparers(base.comparersInUse - 1) + else + comparer + inline def comparing[T](inline op: TypeComparer => T)(using Context): T = util.Stats.record("comparing") val saved = ctx.base.comparersInUse diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 5716b1230e15..2a1cf222c106 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1011,7 +1011,9 @@ class Definitions { @tu lazy val Caps_ContainsModule: Symbol = requiredModule("scala.caps.Contains") @tu lazy val Caps_containsImpl: TermSymbol = Caps_ContainsModule.requiredMethod("containsImpl") @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") - @tu lazy val Caps_SharedCapability: ClassSymbol = requiredClass("scala.caps.Sharable") + @tu lazy val Caps_Sharable: ClassSymbol = requiredClass("scala.caps.Sharable") + @tu lazy val Caps_Control: ClassSymbol = requiredClass("scala.caps.Control") + @tu lazy val Caps_Classifier: ClassSymbol = requiredClass("scala.caps.Classifier") @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") @@ -2095,7 +2097,7 @@ class Definitions { Caps_Capability, // TODO: Remove when Capability is stabilized RequiresCapabilityAnnot, captureRoot, Caps_CapSet, Caps_ContainsTrait, Caps_ContainsModule, Caps_ContainsModule.moduleClass, UseAnnot, - Caps_Mutable, Caps_SharedCapability, ConsumeAnnot, + Caps_Mutable, Caps_Sharable, Caps_Control, Caps_Classifier, ConsumeAnnot, CapsUnsafeModule, CapsUnsafeModule.moduleClass, CapsInternalModule, CapsInternalModule.moduleClass, RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index eb03a2b1c05d..870a73a28689 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3536,16 +3536,16 @@ object TypeComparer { comparing(_.subCaptures(refs1, refs2, vs)) def logUndoAction(action: () => Unit)(using Context): Unit = - comparer.logUndoAction(action) + currentComparer.logUndoAction(action) def inNestedLevel(op: => Boolean)(using Context): Boolean = - comparer.inNestedLevel(op) + currentComparer.inNestedLevel(op) def addErrorNote(note: ErrorNote)(using Context): Unit = - comparer.addErrorNote(note) + currentComparer.addErrorNote(note) def updateErrorNotes(f: PartialFunction[ErrorNote, ErrorNote])(using Context): Unit = - comparer.errorNotes = comparer.errorNotes.mapConserve: p => + currentComparer.errorNotes = currentComparer.errorNotes.mapConserve: p => val (level, note) = p if f.isDefinedAt(note) then (level, f(note)) else p @@ -3553,7 +3553,7 @@ object TypeComparer { comparing(_.compareResult(op)) inline def noNotes(inline op: Boolean)(using Context): Boolean = - comparer.isolated(op, x => x) + currentComparer.isolated(op, x => x) } object MatchReducer: diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 41f8d95a3ebc..3416337e754f 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -481,7 +481,10 @@ class PlainPrinter(_ctx: Context) extends Printer { vbleText ~ Str(hashStr(c.binder)).provided(printDebug) ~ Str(idStr).provided(showUniqueIds) case c: FreshCap => val idStr = if showUniqueIds then s"#${c.rootId}" else "" - if ccVerbose then s"" + def classified = + if c.hiddenSet.classifier == defn.AnyClass then "" + else s" classified as ${c.hiddenSet.classifier.name.show}" + if ccVerbose then s"" else "cap" case tp: TypeProxy => homogenize(tp) match diff --git a/library/src/scala/caps/package.scala b/library/src/scala/caps/package.scala index e787cb0af16b..135437752441 100644 --- a/library/src/scala/caps/package.scala +++ b/library/src/scala/caps/package.scala @@ -25,25 +25,32 @@ import annotation.{experimental, compileTimeOnly, retainsCap} @experimental trait Capability extends Any +/** A marker trait for classifier capabilities that can appear in `.only` + * qualifiers. Capability classes directly extending `Classifier` are treated + * as classifier capbilities + */ +@experimental +trait Classifier + /** The universal capture reference. */ @experimental object cap extends Capability /** Marker trait for classes with methods that requires an exclusive reference. */ @experimental -trait Mutable extends Capability +trait Mutable extends Capability, Classifier /** Marker trait for capabilities that can be safely shared in a concurrent context. * During separation checking, shared capabilities are not taken into account. */ @experimental -trait Sharable extends Capability +trait Sharable extends Capability, Classifier /** Base trait for capabilities that capture some continuation or return point in * the stack. Examples are exceptions, labels, Async, CanThrow. */ @experimental -trait Control extends Sharable +trait Control extends Sharable, Classifier /** Carrier trait for capture set type parameters */ @experimental diff --git a/tests/neg-custom-args/captures/classified-inheritance.check b/tests/neg-custom-args/captures/classified-inheritance.check new file mode 100644 index 000000000000..629f815c4b06 --- /dev/null +++ b/tests/neg-custom-args/captures/classified-inheritance.check @@ -0,0 +1,8 @@ +-- Error: tests/neg-custom-args/captures/classified-inheritance.scala:5:6 ---------------------------------------------- +5 |class C2 extends caps.Control, caps.Mutable // error + | ^ + | class C2 inherits two unrelated classifier traits: trait Mutable and trait Control +-- Error: tests/neg-custom-args/captures/classified-inheritance.scala:10:6 --------------------------------------------- +10 |class C3 extends Matrix, Async // error + | ^ + | class C3 inherits two unrelated classifier traits: trait Control and trait Mutable diff --git a/tests/neg-custom-args/captures/classified-inheritance.scala b/tests/neg-custom-args/captures/classified-inheritance.scala new file mode 100644 index 000000000000..11f342d314a7 --- /dev/null +++ b/tests/neg-custom-args/captures/classified-inheritance.scala @@ -0,0 +1,10 @@ +import language.experimental.captureChecking + +class C1 extends caps.Control, caps.Sharable // OK + +class C2 extends caps.Control, caps.Mutable // error + +trait Async extends caps.Control +class Matrix extends caps.Mutable + +class C3 extends Matrix, Async // error diff --git a/tests/neg-custom-args/captures/classified-wf.check b/tests/neg-custom-args/captures/classified-wf.check new file mode 100644 index 000000000000..9552653d84df --- /dev/null +++ b/tests/neg-custom-args/captures/classified-wf.check @@ -0,0 +1,5 @@ +-- Error: tests/neg-custom-args/captures/classified-wf.scala:7:19 ------------------------------------------------------ +7 |def foo(x: Object^{cap.only[Async]}) = ??? // error + | ^^^^^^^^^^^^^^^ + | scala.caps.cap.only[Async] is not well-formed since class Async is not a classifier class. + | A classifier class is a class extending `caps.Capability` and directly extending `caps.Classifier`. diff --git a/tests/neg-custom-args/captures/classified-wf.scala b/tests/neg-custom-args/captures/classified-wf.scala new file mode 100644 index 000000000000..98a61b2d7b7d --- /dev/null +++ b/tests/neg-custom-args/captures/classified-wf.scala @@ -0,0 +1,8 @@ +import caps.* + +class Async extends Capability + +class IO extends Capability, Classifier + +def foo(x: Object^{cap.only[Async]}) = ??? // error +def bar(x: Object^{cap.only[IO]}) = ??? // ok diff --git a/tests/neg-custom-args/captures/classifiers-1.scala b/tests/neg-custom-args/captures/classifiers-1.scala new file mode 100644 index 000000000000..ee49330ec801 --- /dev/null +++ b/tests/neg-custom-args/captures/classifiers-1.scala @@ -0,0 +1,9 @@ +class M extends caps.Mutable + +class M1(x: Int => Int) extends M // error + +def f(x: M^) = ??? + +def test(g: Int => Int) = f(new M1(g)) // error + + diff --git a/tests/neg-custom-args/captures/shared-capability.check b/tests/neg-custom-args/captures/shared-capability.check index 15355a9fc5b9..0c575cd69b78 100644 --- a/tests/neg-custom-args/captures/shared-capability.check +++ b/tests/neg-custom-args/captures/shared-capability.check @@ -1,6 +1,6 @@ -- 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` + | Async^ extends Sharable, so it cannot capture `cap` | | where: ^ refers to the universal root capability diff --git a/tests/pos-custom-args/captures/restrict-try.scala b/tests/pos-custom-args/captures/restrict-try.scala index eb7a8bca87ed..14848ba98066 100644 --- a/tests/pos-custom-args/captures/restrict-try.scala +++ b/tests/pos-custom-args/captures/restrict-try.scala @@ -1,12 +1,9 @@ -import caps.Capability +import caps.{Capability, Control} class Try[+T] case class Ok[T](x: T) extends Try[T] case class Fail(ex: Exception) extends Try[Nothing] -import util.Try -class Control extends Capability - object Try: def apply[T](body: => T): Try[T]^{body.only[Control]} = try Ok(body) diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index d26047f50cb8..f43c71dfb9da 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -33,6 +33,7 @@ val experimentalDefinitionInLibrary = Set( "scala.Pure", "scala.caps.CapSet", "scala.caps.Capability", + "scala.caps.Classifier", "scala.caps.Contains", "scala.caps.Contains$", "scala.caps.Contains$.containsImpl", From 4b69577b1e31c3ee8983436292a32a3957f32149 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 11 Jul 2025 17:40:36 +0200 Subject: [PATCH 6/7] Implement subsumption rules for restricted capabilities --- .../src/dotty/tools/dotc/cc/Capability.scala | 35 ++++++++++++++++--- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +-- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 1 + 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Capability.scala b/compiler/src/dotty/tools/dotc/cc/Capability.scala index de28f62f42a2..2562ac4ac5a4 100644 --- a/compiler/src/dotty/tools/dotc/cc/Capability.scala +++ b/compiler/src/dotty/tools/dotc/cc/Capability.scala @@ -336,15 +336,20 @@ object Capabilities: case Maybe(ref1) => ref1.stripReadOnly.maybe case _ => this - final def stripRestricted(using Context): Capability = this match - case Restricted(ref1, _) => ref1 - case ReadOnly(ref1) => ref1.stripRestricted.readOnly - case Maybe(ref1) => ref1.stripRestricted.maybe + /** Drop restrictions with clss `cls` or a superclass of `cls` */ + final def stripRestricted(cls: ClassSymbol)(using Context): Capability = this match + case Restricted(ref1, cls1) if cls.isSubClass(cls1) => ref1 + case ReadOnly(ref1) => ref1.stripRestricted(cls).readOnly + case Maybe(ref1) => ref1.stripRestricted(cls).maybe case _ => this + final def stripRestricted(using Context): Capability = + stripRestricted(defn.NothingClass) + final def stripReach(using Context): Capability = this match case Reach(ref1) => ref1 case ReadOnly(ref1) => ref1.stripReach.readOnly + case Restricted(ref1, cls) => ref1.stripReach.restrict(cls) case Maybe(ref1) => ref1.stripReach.maybe case _ => this @@ -541,6 +546,22 @@ object Capabilities: self.classifier.isSubClass(cls) && captureSetOfInfo.tryClassifyAs(cls) + def isKnownClassifiedAs(cls: ClassSymbol)(using Context): Boolean = + transClassifiers match + case ClassifiedAs(cs) => cs.forall(_.isSubClass(cls)) + case _ => false + + def isKnownEmpty(using Context): Boolean = this match + case Restricted(ref1, cls) => + val isEmpty = ref1.transClassifiers match + case ClassifiedAs(cs) => + cs.forall(c => leastClassifier(c, cls) == defn.NothingClass) + case _ => false + isEmpty || ref1.isKnownEmpty + case ReadOnly(ref1) => ref1.isKnownEmpty + case Maybe(ref1) => ref1.isKnownEmpty + case _ => false + def invalidateCaches() = captureSetValid = invalid classifiersValid = invalid @@ -597,6 +618,7 @@ object Capabilities: || viaInfo(y.info)(subsumingRefs(this, _)) case Maybe(y1) => this.stripMaybe.subsumes(y1) case ReadOnly(y1) => this.stripReadOnly.subsumes(y1) + case Restricted(y1, cls) => this.stripRestricted(cls).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`, @@ -611,6 +633,7 @@ object Capabilities: case _ => false || this.match case Reach(x1) => x1.subsumes(y.stripReach) + case Restricted(x1, cls) => y.isKnownClassifiedAs(cls) && x1.subsumes(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) => @@ -652,7 +675,7 @@ object Capabilities: vs.ifNotSeen(this)(x.hiddenSet.elems.exists(_.subsumes(y))) || levelOK && ( y.tryClassifyAs(x.hiddenSet.classifier) - || { capt.println(i"$y is not classified as $x"); false } + || { capt.println(i"$y cannot be classified as $x"); false } ) && canAddHidden && vs.addHidden(x.hiddenSet, y) @@ -674,6 +697,7 @@ object Capabilities: case _ => y match case ReadOnly(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) + case Restricted(y1, cls) => this.stripRestricted(cls).maxSubsumes(y1, canAddHidden) case _ => false /** `x covers y` if we should retain `y` when computing the overlap of @@ -718,6 +742,7 @@ object Capabilities: val c1 = c.underlying.toType c match case _: ReadOnly => ReadOnlyCapability(c1) + case Restricted(_, cls) => OnlyCapability(c1, cls) case _: Reach => ReachCapability(c1) case _: Maybe => MaybeCapability(c1) case _ => c1 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a096f9696f32..d6a58167cb9c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -81,7 +81,7 @@ extension (tp: Type) case ReadOnlyCapability(tp1) => tp1.toCapability.readOnly case OnlyCapability(tp1, cls) => - tp1.toCapability.restrict(cls) // for now + tp1.toCapability.restrict(cls) case ref: TermRef if ref.isCapRef => GlobalCap case ref: Capability if ref.isTrackableRef => @@ -290,7 +290,7 @@ 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: Capability if ref.isTracked || ref.isReach || ref.isReadOnly => + case ref: Capability if ref.isTracked || ref.isInstanceOf[DerivedCapability] => ref.singletonCaptureSet case _ => refs CapturingType(parent, refs1, boxed) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 5dd16e1bdb57..720e1fbcf25d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -210,6 +210,7 @@ sealed abstract class CaptureSet extends Showable: protected def addIfHiddenOrFail(elem: Capability)(using ctx: Context, vs: VarState): Boolean = elems.exists(_.maxSubsumes(elem, canAddHidden = true)) + || elem.isKnownEmpty || failWith(IncludeFailure(this, elem)) /** If this is a variable, add `cs` as a dependent set */ From 971971272fd850efbdea8e94099c99e90b8fc66d Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 12 Jul 2025 10:55:35 +0200 Subject: [PATCH 7/7] Update MimaFilters --- project/MiMaFilters.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index cbfc1affa332..d4367a230aaa 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -9,7 +9,7 @@ object MiMaFilters { // Additions that require a new minor version of the library Build.mimaPreviousDottyVersion -> Seq( ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.readOnlyCapability"), - ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.asCapability"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.onlyCapability"), // Scala.js-only class ProblemFilters.exclude[FinalClassProblem]("scala.scalajs.runtime.AnonFunctionXXL"),