Skip to content

Commit

Permalink
Disallow covariant caps in the lower bound of type members (#19624)
Browse files Browse the repository at this point in the history
Fixes #19330.

Since when instantiating a type member we do not disallow covariant
`cap`s in the instance, a check is added at the application site to
check for covariant `cap`s in the lower bound of type members to
maintain soundness. This check is elided for type parameters since their
instances are always checked at the instantiation site.
  • Loading branch information
Linyxus authored Feb 15, 2024
2 parents 50d62f7 + d539d89 commit 0800dec
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 20 deletions.
27 changes: 7 additions & 20 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -142,32 +142,19 @@ object CheckCaptures:

private val seen = new EqHashSet[TypeRef]

/** Check that there is at least one method containing carrier and defined
* in the scope of tparam. E.g. this is OK:
* def f[T] = { ... var x: T ... }
* So is this:
* class C[T] { def f() = { class D { var x: T }}}
* But this is not OK:
* class C[T] { object o { var x: T }}
*/
extension (tparam: Symbol) def isParametricIn(carrier: Symbol): Boolean =
carrier.exists && {
val encl = carrier.owner.enclosingMethodOrClass
if encl.isClass then tparam.isParametricIn(encl)
else
def recur(encl: Symbol): Boolean =
if tparam.owner == encl then true
else if encl.isStatic || !encl.exists then false
else recur(encl.owner.enclosingMethodOrClass)
recur(encl)
}

def traverse(t: Type) =
t.dealiasKeepAnnots match
case t: TypeRef =>
if !seen.contains(t) then
seen += t
traverseChildren(t)

// Check the lower bound of path dependent types.
// See issue #19330.
val isMember = t.prefix ne NoPrefix
t.info match
case TypeBounds(lo, _) if isMember => traverse(lo)
case _ =>
case AnnotatedType(_, ann) if ann.symbol == defn.UncheckedCapturesAnnot =>
()
case t =>
Expand Down
14 changes: 14 additions & 0 deletions tests/neg-custom-args/captures/i19330-alt.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import language.experimental.captureChecking

trait Logger
def usingLogger[T](op: Logger^ => T): T = ???

def foo[T >: () => Logger^](): T =
val leaked = usingLogger[T]: l => // ok
val t: () => Logger^ = () => l
t: T
leaked

def test(): Unit =
val bad: () => Logger^ = foo[() => Logger^] // error
val leaked: Logger^ = bad() // leaked scoped capability!
13 changes: 13 additions & 0 deletions tests/neg-custom-args/captures/i19330-alt2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import language.experimental.captureChecking

trait Logger
def usingLogger[T](op: Logger^ => T): T = ???

trait Foo:
type T >: () => Logger^

def foo: this.T =
val leaked = usingLogger[T]: l => // error
val t: () => Logger^ = () => l
t: T
leaked
21 changes: 21 additions & 0 deletions tests/neg-custom-args/captures/i19330.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import language.experimental.captureChecking

trait Logger
def usingLogger[T](op: Logger^ => T): T = ???

trait Foo:
type T >: () => Logger^

class Bar extends Foo:
type T = () => Logger^

def foo(x: Foo): x.T =
val leaked = usingLogger[x.T]: l => // error
val t: () => Logger^ = () => l
t: x.T
leaked

def test(): Unit =
val bar = new Bar
val bad: bar.T = foo(bar)
val leaked: Logger^ = bad() // leaked scoped capability!

0 comments on commit 0800dec

Please sign in to comment.