diff --git a/compiler/src/dotty/tools/dotc/cc/Capability.scala b/compiler/src/dotty/tools/dotc/cc/Capability.scala index 5a6fb7df5511..3d0f885176a6 100644 --- a/compiler/src/dotty/tools/dotc/cc/Capability.scala +++ b/compiler/src/dotty/tools/dotc/cc/Capability.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 42e7f67de460..6018cfec1d05 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -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. @@ -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) @@ -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 */ @@ -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 @@ -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) @@ -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 = @@ -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 diff --git a/compiler/src/dotty/tools/dotc/cc/ccConfig.scala b/compiler/src/dotty/tools/dotc/cc/ccConfig.scala index 57ff55fb5c6e..d7cdaa40786c 100644 --- a/compiler/src/dotty/tools/dotc/cc/ccConfig.scala +++ b/compiler/src/dotty/tools/dotc/cc/ccConfig.scala @@ -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 diff --git a/tests/neg-custom-args/captures/capless-strawman1.scala b/tests/neg-custom-args/captures/capless-strawman1.scala new file mode 100644 index 000000000000..164311ca878c --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman1.scala @@ -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 diff --git a/tests/neg-custom-args/captures/capless-strawman2.check b/tests/neg-custom-args/captures/capless-strawman2.check new file mode 100644 index 000000000000..71d65dd21ac9 --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman2.check @@ -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` diff --git a/tests/neg-custom-args/captures/capless-strawman2.scala b/tests/neg-custom-args/captures/capless-strawman2.scala new file mode 100644 index 000000000000..81c46b9d4069 --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman2.scala @@ -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 diff --git a/tests/neg-custom-args/captures/capless-strawman3.check b/tests/neg-custom-args/captures/capless-strawman3.check new file mode 100644 index 000000000000..17f73206ef27 --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman3.check @@ -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` diff --git a/tests/neg-custom-args/captures/capless-strawman3.scala b/tests/neg-custom-args/captures/capless-strawman3.scala new file mode 100644 index 000000000000..59d6252f7e53 --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman3.scala @@ -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() diff --git a/tests/neg-custom-args/captures/capless-strawman4.check b/tests/neg-custom-args/captures/capless-strawman4.check new file mode 100644 index 000000000000..9410bb7fc80e --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman4.check @@ -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` diff --git a/tests/neg-custom-args/captures/capless-strawman4.scala b/tests/neg-custom-args/captures/capless-strawman4.scala new file mode 100644 index 000000000000..84f36dfab57b --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman4.scala @@ -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 diff --git a/tests/neg-custom-args/captures/capless-strawman5.check b/tests/neg-custom-args/captures/capless-strawman5.check new file mode 100644 index 000000000000..641d81128af0 --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman5.check @@ -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` diff --git a/tests/neg-custom-args/captures/capless-strawman5.scala b/tests/neg-custom-args/captures/capless-strawman5.scala new file mode 100644 index 000000000000..2e7506679425 --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman5.scala @@ -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 diff --git a/tests/neg-custom-args/captures/capless-strawman6.check b/tests/neg-custom-args/captures/capless-strawman6.check new file mode 100644 index 000000000000..5b68f7950754 --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman6.check @@ -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` diff --git a/tests/neg-custom-args/captures/capless-strawman6.scala b/tests/neg-custom-args/captures/capless-strawman6.scala new file mode 100644 index 000000000000..0d46d112f4aa --- /dev/null +++ b/tests/neg-custom-args/captures/capless-strawman6.scala @@ -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