Skip to content

Flag nonsensical capturing types with pure parents as errors #23367

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,9 @@ extension (tp: Type)
case tp: (TypeRef | AppliedType) =>
val sym = tp.typeSymbol
if sym.isClass then sym.isPureClass
else tp.superType.isAlwaysPure
else !tp.superType.isAny && tp.superType.isAlwaysPure
case tp: TypeProxy =>
tp.superType.isAlwaysPure
!tp.superType.isAny && tp.superType.isAlwaysPure
case tp: AndType =>
tp.tp1.isAlwaysPure || tp.tp2.isAlwaysPure
case tp: OrType =>
Expand Down
33 changes: 20 additions & 13 deletions compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -406,19 +406,26 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
CapturingType(fntpe, cs, boxed = false)
else fntpe

/** Check that types extending SharedCapability don't have a `cap` in their capture set.
* TODO This is not enough.
* We need to also track that we cannot get exclusive capabilities in paths
* where some prefix derives from SharedCapability. Also, can we just
* exclude `cap`, or do we have to extend this to all exclusive capabilties?
* The problem is that we know what is exclusive in general only after capture
* checking, not before.
/** 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.
* TODO This is not enough.
* We need to also track that we cannot get exclusive capabilities in paths
* where some prefix derives from SharedCapability. Also, can we just
* exclude `cap`, or do we have to extend this to all exclusive capabilties?
* The problem is that we know what is exclusive in general only after capture
* checking, not before.
*/
def checkSharedOK(tp: Type): tp.type =
def checkRetainsOK(tp: Type): tp.type =
tp match
case CapturingType(parent, refs)
if refs.isUniversal && parent.derivesFromSharedCapability =>
fail(em"$tp extends SharedCapability, so it cannot capture `cap`")
case CapturingType(parent, refs) =>
if parent.isAlwaysPure && !tptToCheck.span.isZeroExtent then
// If tptToCheck is zero-extent it could be copied from an overridden
// method's result type. In that case, there's no point requiring
// an explicit result type in the override, the inherited capture set
// 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`")
case _ =>
tp

Expand All @@ -436,7 +443,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
def innerApply(t: Type) =
t match
case t @ CapturingType(parent, refs) =>
checkSharedOK:
checkRetainsOK:
t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs)
case t @ AnnotatedType(parent, ann) =>
val parent1 = this(parent)
Expand All @@ -445,7 +452,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
if !tptToCheck.isEmpty then
checkWellformedLater(parent2, ann.tree, tptToCheck)
try
checkSharedOK:
checkRetainsOK:
CapturingType(parent2, ann.tree.toCaptureSet)
catch case ex: IllegalCaptureRef =>
if !tptToCheck.isEmpty then
Expand Down
12 changes: 5 additions & 7 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1657,19 +1657,17 @@ object Types extends TypeUtils {
NoType
}

/** The iterator of underlying types as long as type is a TypeProxy.
* Useful for diagnostics
/** The iterator of underlying types staring with `this` and followed by
* repeatedly applying `f` as long as type is a TypeProxy. Useful for diagnostics.
*/
def underlyingIterator(using Context): Iterator[Type] = new Iterator[Type] {
def iterate(f: TypeProxy => Type): Iterator[Type] = new Iterator[Type]:
var current = Type.this
var hasNext = true
def next() = {
def next() =
val res = current
hasNext = current.isInstanceOf[TypeProxy]
if (hasNext) current = current.asInstanceOf[TypeProxy].underlying
if hasNext then current = f(current.asInstanceOf[TypeProxy])
res
}
}

/** A prefix-less refined this or a termRef to a new skolem symbol
* that has the given type as info.
Expand Down
4 changes: 2 additions & 2 deletions scala2-library-cc/src/scala/collection/Iterable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ trait SortedSetFactoryDefaults[+A,
+WithFilterCC[x] <: IterableOps[x, WithFilterCC, WithFilterCC[x]] with Set[x]] extends SortedSetOps[A @uncheckedVariance, CC, CC[A @uncheckedVariance]] {
self: IterableOps[A, WithFilterCC, _] =>

override protected def fromSpecific(coll: IterableOnce[A @uncheckedVariance]^): CC[A @uncheckedVariance]^{coll} = sortedIterableFactory.from(coll)(using ordering)
override protected def fromSpecific(coll: IterableOnce[A @uncheckedVariance]^): CC[A @uncheckedVariance] = sortedIterableFactory.from(coll)(using ordering)
override protected def newSpecificBuilder: mutable.Builder[A @uncheckedVariance, CC[A @uncheckedVariance]] = sortedIterableFactory.newBuilder[A](using ordering)
override def empty: CC[A @uncheckedVariance] = sortedIterableFactory.empty(using ordering)

Expand Down Expand Up @@ -1050,7 +1050,7 @@ trait SortedMapFactoryDefaults[K, +V,
self: IterableOps[(K, V), WithFilterCC, _] =>

override def empty: CC[K, V @uncheckedVariance] = sortedMapFactory.empty(using ordering)
override protected def fromSpecific(coll: IterableOnce[(K, V @uncheckedVariance)]^): CC[K, V @uncheckedVariance]^{coll} = sortedMapFactory.from(coll)(using ordering)
override protected def fromSpecific(coll: IterableOnce[(K, V @uncheckedVariance)]^): CC[K, V @uncheckedVariance] = sortedMapFactory.from(coll)(using ordering)
override protected def newSpecificBuilder: mutable.Builder[(K, V @uncheckedVariance), CC[K, V @uncheckedVariance]] = sortedMapFactory.newBuilder[K, V](using ordering)

override def withFilter(p: ((K, V)) => Boolean): collection.SortedMapOps.WithFilter[K, V, WithFilterCC, UnsortedCC, CC]^{p} =
Expand Down
2 changes: 1 addition & 1 deletion tests/neg-custom-args/captures/exception-definitions.check
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- Error: tests/neg-custom-args/captures/exception-definitions.scala:3:8 -----------------------------------------------
3 | self: Err^ => // error
| ^^^^
|reference cap captured by this self type is not included in the allowed capture set {} of pure base class class Throwable
| Err is a pure type, it makes no sense to add a capture set to it
-- Error: tests/neg-custom-args/captures/exception-definitions.scala:7:12 ----------------------------------------------
7 | val x = c // error
| ^
Expand Down
60 changes: 30 additions & 30 deletions tests/neg-custom-args/captures/i15116.check
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:3:13 ----------------------------------------
3 | val x = Foo(m) // error
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:4:13 ----------------------------------------
4 | val x = Foo(m) // error
| ^^^^^^
| Found: Foo{val m: (Bar.this.m² : String^)}^{Bar.this.m²}
| Found: Foo{val m: (Bar.this.m² : STR^)}^{Bar.this.m²}
| Required: Foo
|
| where: ^ refers to a fresh root capability in the type of value m²
Expand All @@ -12,14 +12,14 @@
| Note that the expected type Foo
| is the previously inferred type of value x
| which is also the type seen in separately compiled sources.
| The new inferred type Foo{val m: (Bar.this.m : String^)}^{Bar.this.m}
| The new inferred type Foo{val m: (Bar.this.m : STR^)}^{Bar.this.m}
| must conform to this type.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:5:13 ----------------------------------------
5 | val x = Foo(m) // error
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:6:13 ----------------------------------------
6 | val x = Foo(m) // error
| ^^^^^^
| Found: Foo{val m: (Baz.this.m² : String^)}^{Baz.this.m²}
| Found: Foo{val m: (Baz.this.m² : STR^)}^{Baz.this.m²}
| Required: Foo
|
| where: ^ refers to a fresh root capability in the type of value m²
Expand All @@ -30,14 +30,14 @@
| Note that the expected type Foo
| is the previously inferred type of value x
| which is also the type seen in separately compiled sources.
| The new inferred type Foo{val m: (Baz.this.m : String^)}^{Baz.this.m}
| The new inferred type Foo{val m: (Baz.this.m : STR^)}^{Baz.this.m}
| must conform to this type.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:7:13 ----------------------------------------
7 | val x = Foo(m) // error
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:8:13 ----------------------------------------
8 | val x = Foo(m) // error
| ^^^^^^
| Found: Foo{val m: (Bar1.this.m² : String^)}^{Bar1.this.m²}
| Found: Foo{val m: (Bar1.this.m² : STR^)}^{Bar1.this.m²}
| Required: Foo
|
| where: ^ refers to a fresh root capability in the type of value m²
Expand All @@ -48,25 +48,25 @@
| Note that the expected type Foo
| is the previously inferred type of value x
| which is also the type seen in separately compiled sources.
| The new inferred type Foo{val m: (Bar1.this.m : String^)}^{Bar1.this.m}
| must conform to this type.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:9:13 ----------------------------------------
9 | val x = Foo(m) // error
| ^^^^^^
| Found: Foo{val m: (Baz2.this.m² : String^)}^{Baz2.this.m²}
| Required: Foo
|
| where: ^ refers to a fresh root capability in the type of value m²
| m is a value in class Foo
| m² is a value in trait Baz2
|
|
| Note that the expected type Foo
| is the previously inferred type of value x
| which is also the type seen in separately compiled sources.
| The new inferred type Foo{val m: (Baz2.this.m : String^)}^{Baz2.this.m}
| The new inferred type Foo{val m: (Bar1.this.m : STR^)}^{Bar1.this.m}
| must conform to this type.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:10:13 ---------------------------------------
10 | val x = Foo(m) // error
| ^^^^^^
| Found: Foo{val m: (Baz2.this.m² : STR^)}^{Baz2.this.m²}
| Required: Foo
|
| where: ^ refers to a fresh root capability in the type of value m²
| m is a value in class Foo
| m² is a value in trait Baz2
|
|
| Note that the expected type Foo
| is the previously inferred type of value x
| which is also the type seen in separately compiled sources.
| The new inferred type Foo{val m: (Baz2.this.m : STR^)}^{Baz2.this.m}
| must conform to this type.
|
| longer explanation available when compiling with `-explain`
11 changes: 6 additions & 5 deletions tests/neg-custom-args/captures/i15116.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
class Foo(m: String^)
class Bar(val m: String^):
class STR
class Foo(m: STR^)
class Bar(val m: STR^):
val x = Foo(m) // error
trait Baz(val m: String^):
trait Baz(val m: STR^):
val x = Foo(m) // error
class Bar1(m: String^):
class Bar1(m: STR^):
val x = Foo(m) // error
trait Baz2(m: String^):
trait Baz2(m: STR^):
val x = Foo(m) // error
4 changes: 4 additions & 0 deletions tests/neg-custom-args/captures/nonsensical-for-pure.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Error: tests/neg-custom-args/captures/nonsensical-for-pure.scala:1:7 ------------------------------------------------
1 |val x: Int^ = 2 // error
| ^^^^
| Int is a pure type, it makes no sense to add a capture set to it
1 change: 1 addition & 0 deletions tests/neg-custom-args/captures/nonsensical-for-pure.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
val x: Int^ = 2 // error
30 changes: 30 additions & 0 deletions tests/pending/pos-custom-args/captures/list-encoding.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package listEncoding
import annotation.retains

class Cap

type Op[T, C] =
T => C => C


class STR // used to be pure class String but then the errors don't show up.

type List[T] =
[C] -> (op: Op[T, C]) -> (s: C) ->{op} C

def nil[T]: List[T] =
[C] => (op: Op[T, C]) => (s: C) => s

def cons[T](hd: T, tl: List[T]): List[T] =
[C] => (op: Op[T, C]) => (s: C) => op(hd)(tl(op)(s))

def foo(c: Cap^) =
def f(x: STR @retains[c.type], y: STR @retains[c.type]) =
cons(x, cons(y, nil)) // error, should this work?
def f_explicit(x: STR @retains[c.type], y: STR @retains[c.type])
: [C] => (op: STR^{x, y} => C => C) -> (s: C) ->{op} C =
cons(x, cons(y, nil)) // error, should this work?
def g(x: STR @retains[c.type], y: Any) =
cons(x, cons(y, nil))
def h(x: STR, y: Any @retains[c.type]) =
cons(x, cons(y, nil))
19 changes: 11 additions & 8 deletions tests/pos-custom-args/captures/capt-depfun.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import annotation.retains
import caps.consume
class C
type Cap = C @retains[caps.cap.type]

type T = (x: Cap) -> String @retains[x.type]
type T = (x: Cap) -> STR @retains[x.type]

type ID[X] = X

val aa: ((x: Cap) -> String @retains[x.type]) = (x: Cap) => ""
class STR(s: String)

def f(y: Cap, z: Cap): String @retains[caps.cap.type] =
val a: ((x: Cap) -> String @retains[x.type]) = (x: Cap) => ""
val aa: ((x: Cap) -> STR @retains[x.type]) = (x: Cap) => STR("")

def f(@consume y: Cap, z: Cap): STR @retains[caps.cap.type] =
val a: ((x: Cap) -> STR @retains[x.type]) = (x: Cap) => STR("")
val b = a(y)
val c: String @retains[y.type] = b
val c: STR @retains[y.type] = b
def g(): C @retains[y.type | z.type] = ???
val d = a(g())

val ac: ((x: Cap) -> ID[String @retains[x.type] -> String @retains[x.type]]) = ???
val bc: String^{y} -> String^{y} = ac(y)
val dc: String -> String^{y, z} = ac(g())
val ac: ((x: Cap) -> ID[STR @retains[x.type] -> STR @retains[x.type]]) = ???
val bc: STR^{y} -> STR^{y} = ac(y)
val dc: STR -> STR^{y, z} = ac(g())
c
8 changes: 5 additions & 3 deletions tests/pos-custom-args/captures/capt-depfun2.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import annotation.retains
import caps.consume
class C
type Cap = C @retains[caps.cap.type]

def f(y: Cap, z: Cap) =
class STR
def f(@consume y: Cap, @consume z: Cap) =
def g(): C @retains[y.type | z.type] = ???
val ac: ((x: Cap) -> Array[String @retains[x.type]]) = ???
val dc: Array[? >: String <: String]^{y, z} = ac(g()) // needs to be inferred
val ac: ((x: Cap) -> Array[STR @retains[x.type]]) = ???
val dc: Array[? >: STR <: STR^]^{y, z} = ac(g()) // needs to be inferred
val ec = ac(y)
2 changes: 1 addition & 1 deletion tests/pos-custom-args/captures/capt2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class C
type Cap = C @retains[caps.cap.type]

def test1() =
val y: String^ = ""
val y: C^ = C()
def x: Object @retains[y.type] = y

def test2() =
Expand Down
7 changes: 4 additions & 3 deletions tests/pos-custom-args/captures/caseclass.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
class C extends caps.Capability
class STR(s: String)
object test1:
case class Ref(x: String^)
case class Ref(x: STR^)

def test(c: C) =
val x1 = Ref("hello")
val x1 = Ref(STR("hello"))
val y = x1 match
case Ref(z) => z
val yc: String = y
val yc: STR = y

object test2:
case class Ref(x: () => Unit)
Expand Down
9 changes: 6 additions & 3 deletions tests/pos-custom-args/captures/i16116.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ object Test {
def apply[A](expr: (CpsTransform[F], C) ?=> A): F[A] = ???
}

def asyncPlus[F[_]](a:Int, b:F[Int])(using cps: CpsTransform[F]): Int^{ cps } =
a + (cps.await(b).asInstanceOf[Int])
class C(x: Int):
def +(that: C): C = ???

def asyncPlus[F[_]](a:C, b:F[C])(using cps: CpsTransform[F]): C^{ cps } =
a + (cps.await(b).asInstanceOf[C])

def testExample1Future(): Unit =
val fr = cpsAsync[Future] {
val y = asyncPlus(1,Future successful 2).asInstanceOf[Int]
val y = asyncPlus(C(1),Future successful C(2)).asInstanceOf[Int]
y+1
}

Expand Down
8 changes: 5 additions & 3 deletions tests/pos-custom-args/captures/i21868.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import language.experimental.captureChecking
import caps.*

class U

trait AbstractWrong:
type C^ <: CapSet // no longer an error, the lower bound becomes CapSet now
def f(): Unit^{C}
def f(): U^{C}

trait Abstract1:
type C^ >: CapSet <: CapSet^
def f(): Unit^{C}
def f(): U^{C}

trait Abstract2:
type C^ <: {cap}
def f(): Unit^{C}
def f(): U^{C}
Loading
Loading