Skip to content

Commit

Permalink
Check that @consumed prefix capabilities are not re-used
Browse files Browse the repository at this point in the history
Also: Fixes to computations of overlapWith and -- on Refs that take
account of pathss, where shorter paths cover deeper ones.
  • Loading branch information
odersky committed Jan 29, 2025
1 parent c1ad0c1 commit 65a1301
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 114 deletions.
26 changes: 26 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/CaptureRef.scala
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,32 @@ trait CaptureRef extends TypeProxy, ValueType:
case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden)
case _ => false

/** `x covers y` if we should retain `y` when computing the overlap of
* two footprints which have `x` respectively `y` as elements.
* We assume that .rd have already been stripped on both sides.
* We have:
*
* x covers x
* x covers y ==> x covers y.f
* x covers y ==> x* covers y*, x? covers y?
* TODO what other clauses from subsumes do we need to port here?
*/
final def covers(y: CaptureRef)(using Context): Boolean =
(this eq y)
|| y.match
case y @ TermRef(ypre: CaptureRef, _) if !y.isCap =>
this.covers(ypre)
case ReachCapability(y1) =>
this match
case ReachCapability(x1) => x1.covers(y1)
case _ => false
case MaybeCapability(y1) =>
this match
case MaybeCapability(x1) => x1.covers(y1)
case _ => false
case _ =>
false

def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] =
CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty)

Expand Down
222 changes: 136 additions & 86 deletions compiler/src/dotty/tools/dotc/cc/SepCheck.scala

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion scala2-library-cc/src/scala/collection/View.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import scala.annotation.{nowarn, tailrec}
import scala.collection.mutable.{ArrayBuffer, Builder}
import scala.collection.immutable.LazyList
import language.experimental.captureChecking
import caps.unsafe.unsafeAssumeSeparate

/** Views are collections whose transformation operations are non strict: the resulting elements
* are evaluated only when the view is effectively traversed (e.g. using `foreach` or `foldLeft`),
Expand Down Expand Up @@ -151,7 +152,8 @@ object View extends IterableFactory[View] {
def apply[A](underlying: Iterable[A]^, p: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, p} =
underlying match {
case filter: Filter[A]^{underlying} if filter.isFlipped == isFlipped =>
new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped)
unsafeAssumeSeparate:
new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped)
case _ => new Filter(underlying, p, isFlipped)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import language.experimental.captureChecking

private[mutable] trait CheckedIndexedSeqView[+A] extends IndexedSeqView[A] {

protected val mutationCount: () => Int
protected val mutationCount: () -> Int

override def iterator: Iterator[A]^{this} = new CheckedIndexedSeqView.CheckedIterator(this, mutationCount())
override def reverseIterator: Iterator[A]^{this} = new CheckedIndexedSeqView.CheckedReverseIterator(this, mutationCount())
Expand All @@ -42,7 +42,7 @@ private[mutable] object CheckedIndexedSeqView {
import IndexedSeqView.SomeIndexedSeqOps

@SerialVersionUID(3L)
private[mutable] class CheckedIterator[A](self: IndexedSeqView[A]^, mutationCount: => Int)
private[mutable] class CheckedIterator[A](self: IndexedSeqView[A]^, mutationCount: -> Int)
extends IndexedSeqView.IndexedSeqViewIterator[A](self) {
private[this] val expectedCount = mutationCount
override def hasNext: Boolean = {
Expand All @@ -52,7 +52,7 @@ private[mutable] object CheckedIndexedSeqView {
}

@SerialVersionUID(3L)
private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A]^, mutationCount: => Int)
private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A]^, mutationCount: -> Int)
extends IndexedSeqView.IndexedSeqViewReverseIterator[A](self) {
private[this] val expectedCount = mutationCount
override def hasNext: Boolean = {
Expand All @@ -62,43 +62,43 @@ private[mutable] object CheckedIndexedSeqView {
}

@SerialVersionUID(3L)
class Id[+A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int)
class Id[+A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int)
extends IndexedSeqView.Id(underlying) with CheckedIndexedSeqView[A]

@SerialVersionUID(3L)
class Appended[+A](underlying: SomeIndexedSeqOps[A]^, elem: A)(protected val mutationCount: () => Int)
class Appended[+A](underlying: SomeIndexedSeqOps[A]^, elem: A)(protected val mutationCount: () -> Int)
extends IndexedSeqView.Appended(underlying, elem) with CheckedIndexedSeqView[A]

@SerialVersionUID(3L)
class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int)
class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int)
extends IndexedSeqView.Prepended(elem, underlying) with CheckedIndexedSeqView[A]

@SerialVersionUID(3L)
class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int)
class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int)
extends IndexedSeqView.Concat[A](prefix, suffix) with CheckedIndexedSeqView[A]

@SerialVersionUID(3L)
class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int)
class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int)
extends IndexedSeqView.Take(underlying, n) with CheckedIndexedSeqView[A]

@SerialVersionUID(3L)
class TakeRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int)
class TakeRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int)
extends IndexedSeqView.TakeRight(underlying, n) with CheckedIndexedSeqView[A]

@SerialVersionUID(3L)
class Drop[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int)
class Drop[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int)
extends IndexedSeqView.Drop[A](underlying, n) with CheckedIndexedSeqView[A]

@SerialVersionUID(3L)
class DropRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int)
class DropRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () -> Int)
extends IndexedSeqView.DropRight[A](underlying, n) with CheckedIndexedSeqView[A]

@SerialVersionUID(3L)
class Map[A, B](underlying: SomeIndexedSeqOps[A]^, f: A => B)(protected val mutationCount: () => Int)
class Map[A, B](underlying: SomeIndexedSeqOps[A]^, f: A => B)(protected val mutationCount: () -> Int)
extends IndexedSeqView.Map(underlying, f) with CheckedIndexedSeqView[B]

@SerialVersionUID(3L)
class Reverse[A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int)
class Reverse[A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () -> Int)
extends IndexedSeqView.Reverse[A](underlying) with CheckedIndexedSeqView[A] {
override def reverse: IndexedSeqView[A] = underlying match {
case x: IndexedSeqView[A] => x
Expand All @@ -107,7 +107,7 @@ private[mutable] object CheckedIndexedSeqView {
}

@SerialVersionUID(3L)
class Slice[A](underlying: SomeIndexedSeqOps[A]^, from: Int, until: Int)(protected val mutationCount: () => Int)
class Slice[A](underlying: SomeIndexedSeqOps[A]^, from: Int, until: Int)(protected val mutationCount: () -> Int)
extends AbstractIndexedSeqView[A] with CheckedIndexedSeqView[A] {
protected val lo = from max 0
protected val hi = (until max 0) min underlying.length
Expand Down
29 changes: 29 additions & 0 deletions tests/neg-custom-args/captures/linear-buffer-2.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:13:13 ---------------------------------------------------
13 | val buf3 = buf.append(3) // error
| ^^^
| Separation failure: Illegal access to {buf} which is hidden by the previous definition
| of value buf1 with type Buffer[Int]^.
| This type hides capabilities {buf}
-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:20:13 ---------------------------------------------------
20 | val buf3 = buf1.append(4) // error
| ^^^^
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
| @consume parameter or was used as a prefix to a @consume method on line 18
| and therefore is no longer available.
-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:28:13 ---------------------------------------------------
28 | val buf3 = buf1.append(4) // error
| ^^^^
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
| @consume parameter or was used as a prefix to a @consume method on line 25
| and therefore is no longer available.
-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:38:13 ---------------------------------------------------
38 | val buf3 = buf1.append(4) // error
| ^^^^
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
| @consume parameter or was used as a prefix to a @consume method on line 33
| and therefore is no longer available.
-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:42:4 ----------------------------------------------------
42 | buf.append(1) // error
| ^^^
| Separation failure: (buf : Buffer[Int]^) appears in a loop, therefore it cannot
| be passed to a @consume parameter or be used as a prefix of a @consume method call.
42 changes: 42 additions & 0 deletions tests/neg-custom-args/captures/linear-buffer-2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import caps.{cap, consume, Mutable}
import language.experimental.captureChecking

class Buffer[T] extends Mutable:
@consume mut def append(x: T): Buffer[T]^ = this // ok

def app[T](@consume buf: Buffer[T]^, elem: T): Buffer[T]^ =
buf.append(elem)

def Test(@consume buf: Buffer[Int]^) =
val buf1: Buffer[Int]^ = buf.append(1)
val buf2 = buf1.append(2) // OK
val buf3 = buf.append(3) // error

def Test2(@consume buf: Buffer[Int]^) =
val buf1: Buffer[Int]^ = buf.append(1)
val buf2 =
if ??? then buf1.append(2) // OK
else buf1.append(3) // OK
val buf3 = buf1.append(4) // error

def Test3(@consume buf: Buffer[Int]^) =
val buf1: Buffer[Int]^ = buf.append(1)
val buf2 = (??? : Int) match
case 1 => buf1.append(2) // OK
case 2 => buf1.append(2)
case _ => buf1.append(3)
val buf3 = buf1.append(4) // error

def Test4(@consume buf: Buffer[Int]^) =
val buf1: Buffer[Int]^ = buf.append(1)
val buf2 = (??? : Int) match
case 1 => buf1.append(2) // OK
case 2 => buf1.append(2)
case 3 => buf1.append(3)
case 4 => buf1.append(4)
case 5 => buf1.append(5)
val buf3 = buf1.append(4) // error

def Test5(@consume buf: Buffer[Int]^) =
while true do
buf.append(1) // error
20 changes: 10 additions & 10 deletions tests/neg-custom-args/captures/linear-buffer.check
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,29 @@
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:19:17 -----------------------------------------------------
19 | val buf3 = app(buf, 3) // error
| ^^^
| Separation failure: Illegal access to (buf : Buffer[Int]^),
| which was passed to a @consume parameter on line 17
| Separation failure: Illegal access to (buf : Buffer[Int]^), which was passed to a
| @consume parameter or was used as a prefix to a @consume method on line 17
| and therefore is no longer available.
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:26:17 -----------------------------------------------------
26 | val buf3 = app(buf1, 4) // error
| ^^^^
| Separation failure: Illegal access to (buf1 : Buffer[Int]^),
| which was passed to a @consume parameter on line 24
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
| @consume parameter or was used as a prefix to a @consume method on line 24
| and therefore is no longer available.
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:34:17 -----------------------------------------------------
34 | val buf3 = app(buf1, 4) // error
| ^^^^
| Separation failure: Illegal access to (buf1 : Buffer[Int]^),
| which was passed to a @consume parameter on line 31
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
| @consume parameter or was used as a prefix to a @consume method on line 31
| and therefore is no longer available.
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:44:17 -----------------------------------------------------
44 | val buf3 = app(buf1, 4) // error
| ^^^^
| Separation failure: Illegal access to (buf1 : Buffer[Int]^),
| which was passed to a @consume parameter on line 39
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
| @consume parameter or was used as a prefix to a @consume method on line 39
| and therefore is no longer available.
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:48:8 ------------------------------------------------------
48 | app(buf, 1) // error
| ^^^
| Separation failure: (buf : Buffer[Int]^) appears in a loop,
| therefore it cannot be passed to a @consume parameter.
| Separation failure: (buf : Buffer[Int]^) appears in a loop, therefore it cannot
| be passed to a @consume parameter or be used as a prefix of a @consume method call.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import caps.cap

class It[A]

class Filter[A](val underlying: It[A]^, val p: A => Boolean) extends It[A]
class Filter[A](val underlying: It[A]^, val p: A ->{cap, underlying} Boolean) extends It[A]
object Filter:
def apply[A](underlying: It[A]^, p: A => Boolean): Filter[A]^{underlying, p} =
underlying match
Expand Down
11 changes: 11 additions & 0 deletions tests/pos-custom-args/captures/filter-iterable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import caps.cap

class It[A]

class Filter[A](val underlying: It[A]^, val p: A ->{cap, underlying} Boolean) extends It[A]
object Filter:
def apply[A](underlying: It[A]^, p: A => Boolean): Filter[A]^{cap, p, underlying} =
underlying match
case filter: Filter[A]^ =>
val x = new Filter(filter.underlying, a => filter.p(a) && p(a))
x: Filter[A]^{filter, p}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import annotation.unchecked.{uncheckedVariance, uncheckedCaptures}
import annotation.tailrec
import caps.cap
import caps.untrackedCaptures
import language.`3.7` // sepchecks on
import caps.unsafe.unsafeAssumeSeparate

/** A strawman architecture for new collections. It contains some
* example collection classes and methods with the intent to expose
Expand Down Expand Up @@ -460,7 +460,11 @@ object CollectionStrawMan5 {
def apply[A](underlying: Iterable[A]^, pp: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, pp} =
underlying match
case filter: Filter[A]^{underlying} =>
new Filter(filter.underlying, a => filter.p(a) && pp(a))
unsafeAssumeSeparate:
// See filter-iterable.scala for a test where a variant of Filter
// works without the unsafeAssumeSeparate. But it requires significant
// changes compared to the version here.
new Filter(filter.underlying, a => filter.p(a) && pp(a))
case _ => new Filter(underlying, pp)

case class Partition[A](val underlying: Iterable[A]^, p: A => Boolean) {
Expand Down

0 comments on commit 65a1301

Please sign in to comment.