Skip to content
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/Capability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,8 @@ object Capabilities:

final def isParamPath(using Context): Boolean = paramPathRoot.exists

final def isThisPath(using Context): Boolean = pathRoot.isInstanceOf[ThisType]

final def ccOwner(using Context): Symbol = this match
case self: ThisType => self.cls
case TermRef(prefix: Capability, _) => prefix.ccOwner
Expand Down
46 changes: 39 additions & 7 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -507,17 +507,21 @@ class CheckCaptures extends Recheck, SymTransformer:
if !accessFromNestedClosure then
checkUseDeclared(c, tree.srcPos)
case _ =>
else
else if !c.isThisPath then // We never need to avoid `this`.
var isRoot: Boolean = false
val underlying = c match
case Reach(c1) => CaptureSet.ofTypeDeeply(c1.widen)
case _ => c.core match
case c1: RootCapability => c1.singletonCaptureSet
case c1: RootCapability =>
isRoot = true
c1.singletonCaptureSet
case c1: CoreCapability =>
CaptureSet.ofType(c1.widen, followResult = ccConfig.useSpanCapset)
capt.println(i"Widen reach $c to $underlying in ${env.owner}")
underlying.disallowBadRoots(NoSymbol): () =>
report.error(em"Local capability $c${env.owner.qualString("in")} cannot have `cap` as underlying capture set", tree.srcPos)
recur(underlying, env, lastEnv)
if !isRoot then // To avoid looping infinitely
recur(underlying, env, lastEnv)

/** Avoid locally defined capability if it is a reach capability or capture set
* parameter. This is the default.
Expand Down Expand Up @@ -548,14 +552,23 @@ class CheckCaptures extends Recheck, SymTransformer:
val included = cs.filter: c =>
val isVisible = isVisibleFromEnv(c.pathOwner, env)
if !isVisible then
if ccConfig.deferredReaches
if ccConfig.deferredReaches || ccConfig.caplessLike
then avoidLocalCapability(c, env, lastEnv)
else avoidLocalReachCapability(c, env)
isVisible
//println(i"Include call or box capture $included from $cs in ${env.owner}/${env.captured}/${env.captured.owner}/${env.kind}")
checkSubset(included, env.captured, tree.srcPos, provenance(env))
capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}")
if !isOfNestedMethod(env) then

// A phantom environment is one that should not stop the propagation of captures.
// An environment is phantom if
// (1) it is `NestedInOwner`, which means it is an environment created during box adaptation,
// or (2) it is a class method, in which case the captures should always be aggregated to the class.
inline def isPhantomEnv: Boolean =
env.kind == EnvKind.NestedInOwner
|| (env.owner.is(Method) && env.outer.owner.isClass)

if !isOfNestedMethod(env) && (!ccConfig.caplessLike || isPhantomEnv) then
val nextEnv = nextEnvToCharge(env)
if nextEnv != null && !nextEnv.owner.isStaticOwner then
recur(included, nextEnv, env)
Expand Down Expand Up @@ -594,6 +607,12 @@ class CheckCaptures extends Recheck, SymTransformer:
if sym.exists && curEnv.kind != EnvKind.Boxed then
markFree(capturedVars(sym).filter(isRetained), tree)

/** Include references captured by the type of the function. */
def includeFunctionCaptures(qualType: Type, tree: Tree)(using Context): Unit =
if ccConfig.caplessLike && !qualType.isAlwaysPure then
val funCaptures = qualType.captureSet
markFree(funCaptures, tree)

/** If `tp` (possibly after widening singletons) is an ExprType
* of a parameterless method, map Result instances in it to Fresh instances
*/
Expand Down Expand Up @@ -724,6 +743,10 @@ class CheckCaptures extends Recheck, SymTransformer:
val selType = mapResultRoots(recheckSelection(tree, qualType, name, disambiguate), tree.symbol)
val selWiden = selType.widen

// Include function captures for parameterless class methods
if tree.symbol.is(Method) && qualType.isParameterless then
includeFunctionCaptures(qualType, tree)

// Don't apply the rule
// - on the LHS of assignments, or
// - if the qualifier or selection type is boxed, or
Expand Down Expand Up @@ -815,12 +838,15 @@ class CheckCaptures extends Recheck, SymTransformer:
val argCaptures =
for (argType, formal) <- argTypes.lazyZip(funType.paramInfos) yield
if formal.hasAnnotation(defn.UseAnnot) then argType.deepCaptureSet else argType.captureSet
if ccConfig.caplessLike then
includeFunctionCaptures(qualType, tree)
appType match
case appType @ CapturingType(appType1, refs)
if qualType.exists
&& !tree.fun.symbol.isConstructor
&& qualCaptures.mightSubcapture(refs)
&& argCaptures.forall(_.mightSubcapture(refs)) =>
&& argCaptures.forall(_.mightSubcapture(refs))
&& !ccConfig.caplessLike =>
val callCaptures = argCaptures.foldLeft(qualCaptures)(_ ++ _)
appType.derivedCapturingType(appType1, callCaptures)
.showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qualType.captureSet} = $result", capt)
Expand Down Expand Up @@ -2098,6 +2124,12 @@ class CheckCaptures extends Recheck, SymTransformer:
if !boxedOwner(env).isContainedIn(croot.symbol.owner) then
checkUseDeclared(c, tree.srcPos)

def checkUse(c: Capability, croot: NamedType) =
if ccConfig.caplessLike then
if !env.owner.isProperlyContainedIn(croot.symbol.owner) then
checkUseDeclared(c, tree.srcPos)
else checkUseUnlessBoxed(c, croot)

def check(cs: CaptureSet): Unit = cs.elems.foreach(checkElem)

def checkElem(c: Capability): Unit =
Expand All @@ -2106,7 +2138,7 @@ class CheckCaptures extends Recheck, SymTransformer:
c match
case Reach(c1) =>
c1.paramPathRoot match
case croot: NamedType => checkUseUnlessBoxed(c, croot)
case croot: NamedType => checkUse(c, croot)
case _ => check(CaptureSet.ofTypeDeeply(c1.widen))
case c: TypeRef =>
c.paramPathRoot match
Expand Down
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/ccConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ object ccConfig:
*/
inline val deferredReaches = false

/** Use the capless-like scheme for capture checking:
* - preciser capture sets for curried function types
* - a different rule for handling application captures
*/
inline val caplessLike = true

/** Check that if a type map (which is not a BiTypeMap) maps initial capture
* set variable elements to themselves it will not map any elements added in
* the future to something else. That is, we can safely use a capture set
Expand Down
6 changes: 6 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import language.experimental.captureChecking
trait IO { def println(msg: String): Unit }
def test1(c: IO^): Unit =
val f: () -> () ->{c} Unit = () => () => c.println("hello") // ok
val f2: () -> Unit = () => f()() // error
val f3: () ->{c} Unit = () => f()() // ok
14 changes: 14 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman2.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Error: tests/neg-custom-args/captures/capless-strawman2.scala:3:53 --------------------------------------------------
3 |def test1(ops: List[() => Unit]): Unit = ops.foreach(op => op()) // error
| ^^^^^^^^^^
| Local reach capability ops* leaks into capture scope of method test1.
| You could try to abstract the capabilities referred to by ops* in a capset variable.
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman2.scala:4:49 -----------------------------
4 |def test2(ops: List[() => Unit]): () ->{} Unit = () => ops.foreach(f => f()) // error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Found: () ->{ops*} Unit
| Required: () -> Unit
|
| Note that capability ops* is not included in capture set {}.
|
| longer explanation available when compiling with `-explain`
5 changes: 5 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import language.experimental.captureChecking
trait IO { def println(msg: String): Unit }
def test1(ops: List[() => Unit]): Unit = ops.foreach(op => op()) // error
def test2(ops: List[() => Unit]): () ->{} Unit = () => ops.foreach(f => f()) // error
def test3(ops: List[() => Unit]): () ->{ops*} Unit = () => ops.foreach(f => f()) // ok
38 changes: 38 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman3.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman3.scala:5:23 -----------------------------
5 | val t2: () -> Unit = () => op()() // error
| ^^^^^^^^^^^^
| Found: () ->{io1} Unit
| Required: () -> Unit
|
| Note that capability io1 is not included in capture set {}.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman3.scala:7:23 -----------------------------
7 | val t3: () -> Unit = () => op()()() // error
| ^^^^^^^^^^^^^^
| Found: () ->{io1, io2} Unit
| Required: () -> Unit
|
| Note that capability io1 is not included in capture set {}.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman3.scala:8:29 -----------------------------
8 | val t3a: () ->{io2} Unit = () => op()()() // error
| ^^^^^^^^^^^^^^
| Found: () ->{io1, io2} Unit
| Required: () ->{io2} Unit
|
| Note that capability io1 is not included in capture set {io2}.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman3.scala:11:23 ----------------------------
11 | val t1: () -> Unit = () => // error
| ^
| Found: () ->{io1} Unit
| Required: () -> Unit
|
| Note that capability io1 is not included in capture set {}.
12 | val f = op()
13 | f()
|
| longer explanation available when compiling with `-explain`
13 changes: 13 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman3.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import language.experimental.captureChecking
trait IO { def println(msg: String): Unit }
def test1(io1: IO^, io2: IO^, op: () -> () ->{io1} () ->{io2} Unit): Unit =
val t1: () -> Unit = () => op()
val t2: () -> Unit = () => op()() // error
val t2a: () ->{io1} Unit = () => op()()
val t3: () -> Unit = () => op()()() // error
val t3a: () ->{io2} Unit = () => op()()() // error
val t3b: () ->{io1, io2} Unit = () => op()()()
def test2(io1: IO^, io2: IO^, op: () -> () ->{io1} () ->{io2} Unit): Unit =
val t1: () -> Unit = () => // error
val f = op()
f()
18 changes: 18 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman4.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman4.scala:10:45 ----------------------------
10 | def run(): Unit = ops.foreach(op => op()) // error
| ^
| Found: Runner^{ops*}
| Required: Runner
|
| Note that capability ops* is not included in capture set {}.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman4.scala:13:26 ----------------------------
13 | def run(): Unit = op() // error
| ^
| Found: Runner^{op}
| Required: Runner
|
| Note that capability op is not included in capture set {}.
|
| longer explanation available when compiling with `-explain`
13 changes: 13 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman4.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import language.experimental.captureChecking
trait IO { def println(msg: String): Unit }
abstract class Runner:
def run(): Unit
def test1(ops: List[() => Unit]): Runner^{ops*} =
new Runner:
def run(): Unit = ops.foreach(op => op())
def test2(ops: List[() => Unit]): Runner^{} =
new Runner:
def run(): Unit = ops.foreach(op => op()) // error
def test3(op: () => Unit): Runner^{} =
new Runner:
def run(): Unit = op() // error
14 changes: 14 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman5.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Error: tests/neg-custom-args/captures/capless-strawman5.scala:8:12 --------------------------------------------------
8 | f(x.eval).eval // error
| ^^^^^^^^^^^^^^
| Local reach capability f* leaks into capture scope of method test1.
| You could try to abstract the capabilities referred to by f* in a capset variable.
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman5.scala:12:4 -----------------------------
12 | () => myIO.println("hello") // error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Found: () ->{myIO} Unit
| Required: () -> Unit
|
| Note that capability myIO is not included in capture set {}.
|
| longer explanation available when compiling with `-explain`
12 changes: 12 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman5.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import language.experimental.captureChecking
trait Lazy[+T]:
def eval: T
trait IO { def println(msg: String): Unit }
def flatMap[A, B](x: Lazy[A]^, f: A => Lazy[B]^): Lazy[B]^{x, f*} = // ok
new Lazy[B] { def eval: B = f(x.eval).eval }
def test1[A, B](x: Lazy[A]^, f: A => Lazy[B]^): B =
f(x.eval).eval // error
def test2(newCap: (z: Int) -> IO^): Unit =
val f: () -> Unit =
val myIO = newCap(42)
() => myIO.println("hello") // error
9 changes: 9 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman6.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capless-strawman6.scala:8:23 -----------------------------
8 | val f: () -> Int = () => this.data // error
| ^^^^^^^^^^^^^^^
| Found: () ->{Foo.this} Int
| Required: () -> Int
|
| Note that capability Foo.this is not included in capture set {}.
|
| longer explanation available when compiling with `-explain`
9 changes: 9 additions & 0 deletions tests/neg-custom-args/captures/capless-strawman6.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import language.experimental.captureChecking
class Foo:
self: Foo^ =>
var data: Int = 42
def foo: Int = self.data // ok

def test(): Unit =
val f: () -> Int = () => this.data // error
val g: () ->{this} Int = () => this.data // ok
Loading