diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index cf03273b4805..b344c43d0fdc 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -706,9 +706,34 @@ object TypeOps: def loop(args: List[Tree], boundss: List[TypeBounds]): Unit = args match case arg :: args1 => boundss match case bounds :: boundss1 => + + // Drop caps.Pure from a bound (1) at the top-level, (2) in an `&`, (3) under a type lambda. + def dropPure(tp: Type): Option[Type] = tp match + case tp @ AndType(tp1, tp2) => + dropPure(tp1) match + case Some(tp1o) => + dropPure(tp2) match + case Some(tp2o) => Some(tp.derivedAndType(tp1o, tp2o)) + case None => Some(tp1o) + case None => + dropPure(tp2) + case tp: HKTypeLambda => + for rt <- dropPure(tp.resType) yield + tp.derivedLambdaType(resType = rt) + case _ => + if tp.typeSymbol == defn.PureClass then None + else Some(tp) + + val relevantBounds = + if Feature.ccEnabled then bounds + else + // Drop caps.Pure from bound, it should be checked only when capture checking is enabled + dropPure(bounds.hi).match + case Some(hi1) => bounds.derivedTypeBounds(bounds.lo, hi1) + case None => TypeBounds(bounds.lo, defn.AnyKindType) arg.tpe match - case TypeBounds(lo, hi) => checkOverlapsBounds(lo, hi, arg, bounds) - case tp => checkOverlapsBounds(tp, tp, arg, bounds) + case TypeBounds(lo, hi) => checkOverlapsBounds(lo, hi, arg, relevantBounds) + case tp => checkOverlapsBounds(tp, tp, arg, relevantBounds) loop(args1, boundss1) case _ => case _ => diff --git a/library/src/scala/caps/Pure.scala b/library/src/scala/caps/Pure.scala index 11d0e3f039e9..f92ce8312f43 100644 --- a/library/src/scala/caps/Pure.scala +++ b/library/src/scala/caps/Pure.scala @@ -6,6 +6,15 @@ import language.experimental.captureChecking /** A marker trait that declares that all inheriting classes are "pure" in the * sense that their values retain no capabilities including capabilities needed * to perform effects. This has formal meaning only under capture checking. + * + * NOTE: If an upper bound is Pure, we check that an argument conforms to it only + * in sources where capture checking is enabled. For instance, + * + * def f[C <: Pure]() + * f[Object]() + * + * would give an error only under capture checking. Pure is also dropped in + * upper bounds if it forms part of an &-type, or is under a type lambda. */ trait Pure: this: Pure => diff --git a/tests/neg-custom-args/captures/puretest.scala b/tests/neg-custom-args/captures/puretest.scala new file mode 100644 index 000000000000..5adea56eb896 --- /dev/null +++ b/tests/neg-custom-args/captures/puretest.scala @@ -0,0 +1,12 @@ +import caps.Pure +class P extends Pure +def foo[C <: Pure]() = () +def bar[T, C <: Iterable[T] & Pure]() = () +def baz[CC[_] <: Pure]() = () +def bam[CC[A] <: Pure & Iterable[A]]() = () +def test = + foo[Int]() // error + bar[Int, List[Int]]() // error + baz[Seq]() // error + bam[Seq]() // error + foo[P]() // OK diff --git a/tests/neg/puretest.scala b/tests/neg/puretest.scala new file mode 100644 index 000000000000..74024378b572 --- /dev/null +++ b/tests/neg/puretest.scala @@ -0,0 +1,12 @@ +import caps.Pure +def foo[C <: Pure]() = () +def bar[T, C <: Iterable[T] & Pure]() = () +def baz[CC[_] <: Pure]() = () +def bam[CC[A] <: Pure & Iterable[A]]() = () +def test = + foo[Int]() + bar[Int, List[Int]]() + bar[Int, Iterator[Int]]() // error + baz[Seq]() + bam[Seq]() + bam[Iterator]() // error diff --git a/tests/pos/puretest.scala b/tests/pos/puretest.scala new file mode 100644 index 000000000000..c7f4827ce38a --- /dev/null +++ b/tests/pos/puretest.scala @@ -0,0 +1,10 @@ +import caps.Pure +def foo[C <: Pure]() = () +def bar[T, C <: Iterable[T] & Pure]() = () +def baz[CC[_] <: Pure]() = () +def bam[CC[A] <: Pure & Iterable[A]]() = () +def test = + foo[Int]() + bar[Int, List[Int]]() + baz[Seq]() + bam[Seq]()