Skip to content
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

Refine rules for capture parameters and members #22000

Merged
merged 13 commits into from
Dec 1, 2024
9 changes: 6 additions & 3 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,13 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
def makeCapsOf(tp: RefTree)(using Context): Tree =
TypeApply(Select(scalaDot(nme.caps), nme.capsOf), tp :: Nil)

def makeCapsBound()(using Context): Tree =
makeRetaining(
// Capture set variable `[C^]` becomes: `[C >: CapSet <: CapSet^{cap}]`
def makeCapsBound()(using Context): TypeBoundsTree =
TypeBoundsTree(
Select(scalaDot(nme.caps), tpnme.CapSet),
Nil, tpnme.retainsCap)
makeRetaining(
Select(scalaDot(nme.caps), tpnme.CapSet),
Nil, tpnme.retainsCap))

def makeConstructor(tparams: List[TypeDef], vparamss: List[List[ValDef]], rhs: Tree = EmptyTree)(using Context): DefDef =
DefDef(nme.CONSTRUCTOR, joinParams(tparams, vparamss), TypeTree(), rhs)
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte
case cr: TermRef => ref(cr)
case cr: TermParamRef => untpd.Ident(cr.paramName).withType(cr)
case cr: ThisType => This(cr.cls)
// TODO: Will crash if the type is an annotated type, for example `cap?`
}
val arg = repeated(elems, TypeTree(defn.AnyType))
New(symbol.typeRef, arg :: Nil)
Expand Down
34 changes: 28 additions & 6 deletions compiler/src/dotty/tools/dotc/cc/CaptureRef.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,19 @@ trait CaptureRef extends TypeProxy, ValueType:
final def invalidateCaches() =
myCaptureSetRunId = NoRunId

/** x subsumes x
* this subsumes this.f
noti0na1 marked this conversation as resolved.
Show resolved Hide resolved
/** x subsumes x
* x =:= y ==> x subsumes y
* x subsumes y ==> x subsumes y.f
* x subsumes y ==> x* subsumes y, x subsumes y?
* x subsumes y ==> x* subsumes y*, x? subsumes y?
* x: x1.type /\ x1 subsumes y ==> x subsumes y
* TODO: Document path cases
* X = CapSet^cx, exists rx in cx, rx subsumes y ==> X subsumes y
* Y = CapSet^cy, forall ry in cy, x subsumes ry ==> x subsumes Y
* X: CapSet^c1...CapSet^c2, (CapSet^c1) subsumes y ==> X subsumes y
* Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y
* Contains[X, y] ==> X subsumes y
*
* TODO: Document cases with more comments.
*/
final def subsumes(y: CaptureRef)(using Context): Boolean =

Expand Down Expand Up @@ -135,14 +142,29 @@ trait CaptureRef extends TypeProxy, ValueType:
case _ => false
|| viaInfo(y.info)(subsumingRefs(this, _))
case MaybeCapability(y1) => this.stripMaybe.subsumes(y1)
case y: TypeRef if y.derivesFrom(defn.Caps_CapSet) =>
// The upper and lower bounds don't have to be in the form of `CapSet^{...}`.
// They can be other capture set variables, which are bounded by `CapSet`,
// like `def test[X^, Y^, Z >: X <: Y]`.
y.info match
case TypeBounds(_, hi: CaptureRef) => this.subsumes(hi)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this case happen? I thought the upper bound has to be of the form CapSet^cs? If it can happen, maybe add a comment explaining why.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upper and lower bounds don't have to be in the form of CapSet^{...}. They can also be other capture set variables, which are bounded by CapSet, like def test[X^, Y^, Z >: X <: Y]. With this set up, both cases would happen during subsumes.

case _ => y.captureSetOfInfo.elems.forall(this.subsumes)
case CapturingType(parent, refs) if parent.derivesFrom(defn.Caps_CapSet) =>
refs.elems.forall(this.subsumes)
case _ => false
|| this.match
case ReachCapability(x1) => x1.subsumes(y.stripReach)
case x: TermRef => viaInfo(x.info)(subsumingRefs(_, y))
case x: TermParamRef => subsumesExistentially(x, y)
case x: TypeRef if x.symbol.info.derivesFrom(defn.Caps_CapSet) =>
x.captureSetOfInfo.elems.exists(_.subsumes(y))
case x: TypeRef => assumedContainsOf(x).contains(y)
case x: TypeRef if assumedContainsOf(x).contains(y) => true
case x: TypeRef if x.derivesFrom(defn.Caps_CapSet) =>
x.info match
case TypeBounds(lo: CaptureRef, _) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above; can this case happen?

lo.subsumes(y)
case _ =>
x.captureSetOfInfo.elems.exists(_.subsumes(y))
case CapturingType(parent, refs) if parent.derivesFrom(defn.Caps_CapSet) =>
refs.elems.exists(_.subsumes(y))
case _ => false
end subsumes

Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ sealed abstract class CaptureSet extends Showable:
def debugInfo(using Context) = i"$this accountsFor $x, which has capture set ${x.captureSetOfInfo}"
def test(using Context) = reporting.trace(debugInfo):
elems.exists(_.subsumes(x))
|| !x.isMaxCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK
|| !x.isMaxCapability
&& !x.derivesFrom(defn.Caps_CapSet)
&& x.captureSetOfInfo.subCaptures(this, frozen = true).isOK
comparer match
case comparer: ExplainingTypeComparer => comparer.traceIndented(debugInfo)(test)
case _ => test
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
if others.accountsFor(ref) then
report.warning(em"redundant capture: $dom already accounts for $ref", pos)

if ref.captureSetOfInfo.elems.isEmpty && !ref.derivesFrom(defn.Caps_Capability) then
if ref.captureSetOfInfo.elems.isEmpty
&& !ref.derivesFrom(defn.Caps_Capability)
&& !ref.derivesFrom(defn.Caps_CapSet) then
val deepStr = if ref.isReach then " deep" else ""
report.error(em"$ref cannot be tracked since its$deepStr capture set is empty", pos)
check(parent.captureSet, parent)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2240,7 +2240,7 @@ object Parsers {
atSpan(in.offset):
if in.isIdent(nme.UPARROW) && Feature.ccEnabled then
in.nextToken()
TypeBoundsTree(EmptyTree, makeCapsBound())
makeCapsBound()
else
TypeBoundsTree(bound(SUPERTYPE), bound(SUBTYPE))

Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/RefChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import config.MigrationVersion
import config.Printers.refcheck
import reporting.*
import Constants.Constant
import cc.stripCapturing

object RefChecks {
import tpd.*
Expand Down Expand Up @@ -84,7 +85,7 @@ object RefChecks {
* (Forwarding tends to hide problems by binding parameter names).
*/
private def upwardsThisType(cls: Symbol)(using Context) = cls.info match {
case ClassInfo(_, _, _, _, tp: Type) if (tp ne cls.typeRef) && !cls.isOneOf(FinalOrModuleClass) =>
case ClassInfo(_, _, _, _, tp: Type) if (tp.stripCapturing ne cls.typeRef) && !cls.isOneOf(FinalOrModuleClass) =>
SkolemType(cls.appliedRef).withName(nme.this_)
case _ =>
cls.thisType
Expand Down
6 changes: 3 additions & 3 deletions library/src/scala/caps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@ import annotation.{experimental, compileTimeOnly, retainsCap}
/** A type constraint expressing that the capture set `C` needs to contain
* the capability `R`
*/
sealed trait Contains[C <: CapSet @retainsCap, R <: Singleton]
sealed trait Contains[+C >: CapSet <: CapSet @retainsCap, R <: Singleton]

/** The only implementation of `Contains`. The constraint that `{R} <: C` is
* added separately by the capture checker.
*/
given containsImpl[C <: CapSet @retainsCap, R <: Singleton]: Contains[C, R]()
given containsImpl[C >: CapSet <: CapSet @retainsCap, R <: Singleton]: Contains[C, R]()

/** A wrapper indicating a type variable in a capture argument list of a
* @retains annotation. E.g. `^{x, Y^}` is represented as `@retains(x, capsOf[Y])`.
*/
@compileTimeOnly("Should be be used only internally by the Scala compiler")
def capsOf[CS]: Any = ???
def capsOf[CS >: CapSet <: CapSet @retainsCap]: Any = ???

/** Reach capabilities x* which appear as terms in @retains annotations are encoded
* as `caps.reachCapability(x)`. When converted to CaptureRef types in capture sets
Expand Down
6 changes: 3 additions & 3 deletions scala2-library-cc/src/scala/collection/Stepper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ trait Stepper[@specialized(Double, Int, Long) +A] {
*
* See method `trySplit` in [[java.util.Spliterator]].
*/
def trySplit(): Stepper[A]
def trySplit(): Stepper[A]^{this}

/** Returns an estimate of the number of elements of this Stepper, or [[Long.MaxValue]]. See
* method `estimateSize` in [[java.util.Spliterator]].
Expand All @@ -71,15 +71,15 @@ trait Stepper[@specialized(Double, Int, Long) +A] {
* a [[java.util.Spliterator.OfInt]] (which is a `Spliterator[Integer]`) in the subclass [[IntStepper]]
* (which is a `Stepper[Int]`).
*/
def spliterator[B >: A]: Spliterator[_]
def spliterator[B >: A]: Spliterator[_]^{this}

/** Returns a Java [[java.util.Iterator]] corresponding to this Stepper.
*
* Note that the return type is `Iterator[_]` instead of `Iterator[A]` to allow returning
* a [[java.util.PrimitiveIterator.OfInt]] (which is a `Iterator[Integer]`) in the subclass
* [[IntStepper]] (which is a `Stepper[Int]`).
*/
def javaIterator[B >: A]: JIterator[_]
def javaIterator[B >: A]: JIterator[_]^{this}

/** Returns an [[Iterator]] corresponding to this Stepper. Note that Iterators corresponding to
* primitive Steppers box the elements.
Expand Down
13 changes: 13 additions & 0 deletions tests/neg-custom-args/captures/capset-bound2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import caps.*

class IO

def f[C^](io: IO^{C^}) = ???

def test =
f[CapSet](???)
f[CapSet^{}](???)
f[CapSet^](???)
f[Nothing](???) // error
f[String](???) // error

30 changes: 30 additions & 0 deletions tests/neg-custom-args/captures/capset-members.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import caps.*

trait Abstract[X^]:
type C >: X <: CapSet^
// Don't test the return type using Unit, because it is a pure type.
def boom(): AnyRef^{C^}

class Concrete extends Abstract[CapSet^{}]:
type C = CapSet^{}
// TODO: Why do we get error without the return type here?
def boom(): AnyRef = new Object

class Concrete2 extends Abstract[CapSet^{}]:
type C = CapSet^{}
def boom(): AnyRef^ = new Object // error

class Concrete3 extends Abstract[CapSet^{}]:
def boom(): AnyRef = new Object

class Concrete4(a: AnyRef^) extends Abstract[CapSet^{a}]:
type C = CapSet // error
def boom(): AnyRef^{a} = a // error

class Concrete5(a: AnyRef^, b: AnyRef^) extends Abstract[CapSet^{a}]:
type C = CapSet^{a}
def boom(): AnyRef^{b} = b // error

class Concrete6(a: AnyRef^, b: AnyRef^) extends Abstract[CapSet^{a}]:
def boom(): AnyRef^{b} = b // error

9 changes: 9 additions & 0 deletions tests/neg-custom-args/captures/capture-parameters.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import caps.*

class C

def test[X^, Y^, Z >: X <: Y](x: C^{X^}, y: C^{Y^}, z: C^{Z^}) =
val x2z: C^{Z^} = x
val z2y: C^{Y^} = z
val x2y: C^{Y^} = x // error

2 changes: 1 addition & 1 deletion tests/neg-custom-args/captures/capture-poly.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import caps.*
trait Foo extends Capability

trait CaptureSet:
type C <: CapSet^
type C >: CapSet <: CapSet^

def capturePoly[C^](a: Foo^{C^}): Foo^{C^} = a
def capturePoly2(c: CaptureSet)(a: Foo^{c.C^}): Foo^{c.C^} = a
Expand Down
17 changes: 8 additions & 9 deletions tests/neg-custom-args/captures/i21868.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import caps.*

trait AbstractWrong:
type C <: CapSet
def boom(): Unit^{C^} // error
type C <: CapSet
def f(): Unit^{C^} // error

trait Abstract:
type C <: CapSet^
def boom(): Unit^{C^}

class Concrete extends Abstract:
type C = Nothing
def boom() = () // error
trait Abstract1:
type C >: CapSet <: CapSet^
def f(): Unit^{C^}

// class Abstract2:
// type C^
// def f(): Unit^{C^}
49 changes: 49 additions & 0 deletions tests/neg-custom-args/captures/i21868b.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import language.experimental.modularity
import caps.*

class IO

class File

trait Abstract:
type C >: CapSet <: CapSet^
def f(file: File^{C^}): Unit

class Concrete1 extends Abstract:
type C = CapSet
def f(file: File) = ()

class Concrete2(io: IO^) extends Abstract:
type C = CapSet^{io}
def f(file: File^{io}) = ()

class Concrete3(io: IO^) extends Abstract:
type C = CapSet^{io}
def f(file: File) = () // error

trait Abstract2(tracked val io: IO^):
type C >: CapSet <: CapSet^{io}
def f(file: File^{C^}): Unit

class Concrete4(io: IO^) extends Abstract2(io):
type C = CapSet
def f(file: File) = ()

class Concrete5(io1: IO^, io2: IO^) extends Abstract2(io1):
type C = CapSet^{io2} // error
def f(file: File^{io2}) = ()

trait Abstract3[X^]:
type C >: CapSet <: X
def f(file: File^{C^}): Unit

class Concrete6(io: IO^) extends Abstract3[CapSet^{io}]:
type C = CapSet
def f(file: File) = ()

class Concrete7(io1: IO^, io2: IO^) extends Abstract3[CapSet^{io1}]:
type C = CapSet^{io2} // error
def f(file: File^{io2}) = ()

class Concrete8(io1: IO^, io2: IO^) extends Abstract3[CapSet^{io1}]:
def f(file: File^{io2}) = () // error
8 changes: 8 additions & 0 deletions tests/neg-custom-args/captures/i22005.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import caps.*

class IO
class File(io: IO^)

class Handler[C^]:
def f(file: File^): File^{C^} = file // error
def g(file: File^{C^}): File^ = file // ok
16 changes: 8 additions & 8 deletions tests/neg-custom-args/captures/use-capset.check
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
-- Error: tests/neg-custom-args/captures/use-capset.scala:7:50 ---------------------------------------------------------
7 |private def g[C^] = (xs: List[Object^{C^}]) => xs.head // error
-- Error: tests/neg-custom-args/captures/use-capset.scala:5:50 ---------------------------------------------------------
5 |private def g[C^] = (xs: List[Object^{C^}]) => xs.head // error
| ^^^^^^^
| Capture set parameter C leaks into capture scope of method g.
| To allow this, the type C should be declared with a @use annotation
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/use-capset.scala:13:22 -----------------------------------
13 | val _: () -> Unit = h // error: should be ->{io}
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/use-capset.scala:11:22 -----------------------------------
11 | val _: () -> Unit = h // error: should be ->{io}
| ^
| Found: (h : () ->{io} Unit)
| Required: () -> Unit
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/use-capset.scala:15:50 -----------------------------------
15 | val _: () -> List[Object^{io}] -> Object^{io} = h2 // error, should be ->{io}
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/use-capset.scala:13:50 -----------------------------------
13 | val _: () -> List[Object^{io}] -> Object^{io} = h2 // error, should be ->{io}
| ^^
| Found: (h2 : () ->? (x$0: List[box Object^]^{}) ->{io} Object^{io})
| Required: () -> List[box Object^{io}] -> Object^{io}
| Found: (h2 : () ->? (x$0: List[box Object^{io}]^{}) ->{io} Object^{io})
| Required: () -> List[box Object^{io}] -> Object^{io}
|
| longer explanation available when compiling with `-explain`
2 changes: 0 additions & 2 deletions tests/neg-custom-args/captures/use-capset.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import caps.{use, CapSet}



def f[C^](@use xs: List[Object^{C^}]): Unit = ???

private def g[C^] = (xs: List[Object^{C^}]) => xs.head // error
Expand Down
7 changes: 0 additions & 7 deletions tests/neg/cc-poly-2.check
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
-- [E007] Type Mismatch Error: tests/neg/cc-poly-2.scala:13:15 ---------------------------------------------------------
13 | f[Nothing](d) // error
| ^
| Found: (d : Test.D^)
| Required: Test.D
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg/cc-poly-2.scala:14:19 ---------------------------------------------------------
14 | f[CapSet^{c1}](d) // error
| ^
Expand Down
2 changes: 1 addition & 1 deletion tests/neg/cc-poly-2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ object Test:

def test(c1: C, c2: C) =
val d: D^ = D()
f[Nothing](d) // error
// f[Nothing](d) // already ruled out at typer
f[CapSet^{c1}](d) // error
val x = f(d)
val _: D^{c1} = x // error
Loading
Loading