Skip to content

Adjustments to the capability trilogy #23428

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions compiler/src/dotty/tools/dotc/cc/Capability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import annotation.constructorOnly
import ast.tpd
import printing.{Printer, Showable}
import printing.Texts.Text
import reporting.Message
import reporting.{Message, trace}
import NameOps.isImpureFunction
import annotation.internal.sharable

Expand Down Expand Up @@ -693,7 +693,7 @@ object Capabilities:
thisMap =>

override def apply(t: Type) =
if variance <= 0 then t
if variance < 0 then t
else t match
case t @ CapturingType(_, _) =>
mapOver(t)
Expand All @@ -703,6 +703,8 @@ object Capabilities:
this(CapturingType(parent1, ann.tree.toCaptureSet))
else
t.derivedAnnotatedType(parent1, ann)
case defn.RefinedFunctionOf(_) =>
t // stop at dependent function types
case _ =>
mapFollowingAliases(t)

Expand Down Expand Up @@ -784,7 +786,7 @@ object Capabilities:
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`.
t // `t` should be mapped in this case by a different call to `toResult`. See [[toResultInResults]].
case t: (LazyRef | TypeVar) =>
mapConserveSuper(t)
case _ =>
Expand Down Expand Up @@ -849,7 +851,8 @@ object Capabilities:
end toResult

/** Map global roots in function results to result roots. Also,
* map roots in the types of parameterless def methods.
* map roots in the types of def methods that are parameterless
* or have only type parameters.
*/
def toResultInResults(sym: Symbol, fail: Message => Unit, keepAliases: Boolean = false)(tp: Type)(using Context): Type =
val m = new TypeMap with FollowAliasesMap:
Expand Down Expand Up @@ -878,8 +881,19 @@ object Capabilities:
throw ex
m(tp) match
case tp1: ExprType if sym.is(Method, butNot = Accessor) =>
// Map the result of parameterless `def` methods.
tp1.derivedExprType(toResult(tp1.resType, tp1, fail))
case tp1: PolyType if !tp1.resType.isInstanceOf[MethodicType] =>
// Map also the result type of method with only type parameters.
// This way, the `^` in the following method will be mapped to a `ResultCap`:
// ```
// object Buffer:
// def empty[T]: Buffer[T]^
// ```
// This is more desirable than interpreting `^` as a `Fresh` at the level of `Buffer.empty`
// in most cases.
tp1.derivedLambdaType(resType = toResult(tp1.resType, tp1, fail))
case tp1 => tp1
end toResultInResults

end Capabilities
end Capabilities
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import printing.{Showable, Printer}
import printing.Texts.*
import util.{SimpleIdentitySet, Property}
import typer.ErrorReporting.Addenda
import util.common.alwaysTrue
import scala.collection.{mutable, immutable}
import TypeComparer.ErrorNote
import CCState.*
Expand Down
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import reporting.Message
import printing.{Printer, Texts}, Texts.{Text, Str}
import collection.mutable
import CCState.*
import dotty.tools.dotc.util.NoSourcePosition
import CheckCaptures.CheckerAPI
import NamerOps.methodType
import NameKinds.{CanThrowEvidenceName, TryOwnerName}
Expand Down
4 changes: 2 additions & 2 deletions tests/neg-custom-args/captures/boundschecks3.check
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
| ^
| Type argument test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[test.Tree^]
|
| where: ^ refers to the universal root capability
| where: ^ refers to a fresh root capability in the type of value foo
|
| 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 test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[test.Tree^]
|
| where: ^ refers to the universal root capability
| where: ^ refers to a fresh root capability in the type of type T
|
| longer explanation available when compiling with `-explain`
-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:11:11 --------------------------------
Expand Down
11 changes: 0 additions & 11 deletions tests/neg-custom-args/captures/box-adapt-cases.check
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
-- [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: Cap^?) => Int
| Required: Cap^ =>² Int
|
| where: => refers to the universal root capability
| =>² refers to a fresh root capability created in method test1
| ^ refers to the universal root capability
|
| 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
| ^^^^^^^^^^^^^^^^
Expand Down
2 changes: 1 addition & 1 deletion tests/neg-custom-args/captures/box-adapt-cases.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def test1(): Unit = {
class Id[X](val value: [T] -> (op: X => T) -> T)

val x: Id[Cap^] = ???
x.value(cap => cap.use()) // error, was OK
x.value(cap => cap.use())
}

def test2(io: Cap^): Unit = {
Expand Down
30 changes: 30 additions & 0 deletions tests/neg-custom-args/captures/cc-fresh-levels.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Flag -source set repeatedly
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-fresh-levels.scala:14:10 ------------------------------
14 | r.put(x) // error
| ^
| Found: IO^{x}
| Required: IO^
|
| where: ^ refers to a fresh root capability in the type of value r
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-fresh-levels.scala:17:10 ------------------------------
17 | r.put(innerIO) // error
| ^^^^^^^
| Found: IO^{innerIO}
| Required: IO^
|
| where: ^ refers to a fresh root capability in the type of value r
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-fresh-levels.scala:18:9 -------------------------------
18 | runIO: innerIO => // error
| ^
|Found: (innerIO: IO^?) ->? Unit
|Required: IO^ => Unit
|
|where: => refers to a fresh root capability created in method test1 when checking argument to parameter op of method runIO
| ^ refers to the universal root capability
19 | r.put(innerIO)
|
| longer explanation available when compiling with `-explain`
19 changes: 19 additions & 0 deletions tests/neg-custom-args/captures/cc-fresh-levels.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//> using options -source 3.7
import language.experimental.captureChecking
import caps.*
class IO
class Ref[X](init: X):
private var _data = init
def get: X = _data
def put(y: X): Unit = _data = y
def runIO(op: IO^ => Unit): Unit = ()
def test1(a: IO^, b: IO^, c: IO^): Unit =
val r: Ref[IO^] = Ref(a)
r.put(b) // ok
def outer(x: IO^): Unit =
r.put(x) // error
r.put(c) // ok
runIO: (innerIO: IO^) =>
r.put(innerIO) // error
runIO: innerIO => // error
r.put(innerIO)
16 changes: 16 additions & 0 deletions tests/neg-custom-args/captures/class-level-attack.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Error: tests/neg-custom-args/captures/class-level-attack.scala:12:24 ------------------------------------------------
12 | val r: Ref[IO^] = Ref[IO^](io) // error:
| ^^^
| Type variable X of constructor Ref cannot be instantiated to IO^ since
| that type captures the root capability `cap`.
|
| where: ^ refers to the universal root capability
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-level-attack.scala:17:26 ---------------------------
17 | def set(x: IO^) = r.put(x) // error
| ^
| Found: IO^{x}
| Required: IO^
|
| where: ^ refers to a fresh root capability in the type of value r
|
| longer explanation available when compiling with `-explain`
5 changes: 2 additions & 3 deletions tests/neg-custom-args/captures/class-level-attack.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ class C(io: IO^):
//Type variable X of constructor Ref cannot be instantiated to box IO^ since
//that type captures the root capability `cap`.
// where: ^ refers to the universal root capability
val r2: Ref[IO^] = Ref(io) // error:
//Error: Ref[IO^{io}] does not conform to Ref[IO^] (since Refs are invariant)
def set(x: IO^) = r.put(x)
val r2: Ref[IO^] = Ref(io)
def set(x: IO^) = r.put(x) // error

def outer(outerio: IO^) =
val c = C(outerio)
Expand Down
6 changes: 3 additions & 3 deletions tests/neg-custom-args/captures/i16725.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ def usingIO[T](op: IO => T): T = ???
class Wrapper[T](val value: [R] -> (f: T => R) -> R)
def mk[T](x: T): Wrapper[T] = Wrapper([R] => f => f(x))
def useWrappedIO(wrapper: Wrapper[IO]): () -> Unit =
() =>
wrapper.value: io => // error
() => // error
wrapper.value: io =>
io.brewCoffee()
def main(): Unit =
val escaped = usingIO(io => useWrappedIO(mk(io))) // error
val escaped = usingIO(io => useWrappedIO(mk(io)))
escaped() // boom
6 changes: 3 additions & 3 deletions tests/neg-custom-args/captures/i23389.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ package test1:
val thunks: Collection[() => Unit] // that's fine

object FooImpl1 extends Foo:
val thunks: Collection[() => Unit] = Collection.empty // error
val thunks: Collection[() => Unit] = Collection.empty // was error, now ok
val thunks2: Collection[() => Unit] = Collection.empty[() => Unit] // error
val thunks3: Collection[() => Unit] = Collection.empty[() => Unit] // error

Expand All @@ -31,6 +31,6 @@ package test2:
val thunks: Collection[() => Unit] // that's fine

object FooImpl1 extends Foo:
val thunks: Collection[() => Unit] = Collection.empty // error
val thunks: Collection[() => Unit] = Collection.empty // was error, now ok
val thunks2: Collection[() => Unit] = Collection.empty[() => Unit] // error
val thunks3: Collection[() => Unit] = Collection.empty[() => Unit] // error
val thunks3: Collection[() => Unit] = Collection.empty[() => Unit] // error
10 changes: 10 additions & 0 deletions tests/pos-custom-args/captures/cc-def-fresh.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import language.experimental.captureChecking
trait Collection[T]
trait IO
def empty[T]: Collection[T]^ = ???
def emptyAlt[T](): Collection[T]^ = ???
def newIO: IO^ = ???
def test1(): Unit =
val t1: Collection[Int]^ = empty[Int] // ok
val t2: IO^ = newIO // ok
val t3: Collection[Int]^ = emptyAlt[Int]() // ok
16 changes: 16 additions & 0 deletions tests/pos-custom-args/captures/i23421.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import language.experimental.captureChecking
import caps.*

trait Collection[T] extends Mutable:
update def add(elem: T): Unit
update def remove(elem: T): Unit
def get(index: Int): Option[T]

object Collection:
def empty[T]: Collection[T] = ???

trait Foo:
val thunks: Collection[() => Unit] // that's fine

object FooImpl1 extends Foo:
val thunks: Collection[() => Unit] = Collection.empty // was error, now ok
Loading