Skip to content

Commit

Permalink
Don't propagate captures out of curried nested closures
Browse files Browse the repository at this point in the history
  • Loading branch information
odersky committed Jul 4, 2023
1 parent 9c96538 commit 8030851
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 19 deletions.
36 changes: 17 additions & 19 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ast.{tpd, untpd, Trees}
import Trees.*
import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker}
import typer.Checking.{checkBounds, checkAppliedTypesIn}
import util.{SimpleIdentitySet, EqHashMap, SrcPos}
import util.{SimpleIdentitySet, EqHashMap, SrcPos, Property}
import transform.SymUtils.*
import transform.{Recheck, PreRecheck}
import Recheck.*
Expand Down Expand Up @@ -145,6 +145,9 @@ object CheckCaptures:
traverseChildren(t)
check.traverse(tp)

/** Attachment key for boxed curried closures */
val BoxedClosure = Property.Key[Type]

class CheckCaptures extends Recheck, SymTransformer:
thisPhase =>

Expand Down Expand Up @@ -231,29 +234,15 @@ class CheckCaptures extends Recheck, SymTransformer:
if sym.ownersIterator.exists(_.isTerm) then CaptureSet.Var()
else CaptureSet.empty)

/**
class anon1 extends Function1:
def apply: Function1 =
use(x)
class anon2 extends Function1:
use(y)
def apply: Function1 = ...
anon2()
anon1()
*/
/** For all nested environments up to `limit` perform `op` */
/** For all nested environments up to `limit` or a closed environment perform `op` */
def forallOuterEnvsUpTo(limit: Symbol)(op: Env => Unit)(using Context): Unit =
def stopsPropagation(env: Env) =
val sym = env.owner
env.isOutermost
|| false && sym.is(Method) && sym.owner.isTerm && !sym.isConstructor
def recur(env: Env): Unit =
if env.isOpen && env.owner != limit then
op(env)
if !stopsPropagation(env) then
if !env.isOutermost then
var nextEnv = env.outer
if env.owner.isConstructor then
if nextEnv.owner != limit && !stopsPropagation(nextEnv) then
if nextEnv.owner != limit && !nextEnv.isOutermost then
nextEnv = nextEnv.outer
recur(nextEnv)
recur(curEnv)
Expand Down Expand Up @@ -491,6 +480,15 @@ class CheckCaptures extends Recheck, SymTransformer:
recheckDef(mdef, meth)
meth.updateInfoBetween(preRecheckPhase, thisPhase, completer)
case _ =>
mdef.rhs match
case rhs @ closure(_, _, _) =>
// In a curried closure `x => y => e` don't leak capabilities retained by
// the second closure `y => e` into the first one. This is an approximation
// of the CC rule which says that a closure contributes captures to its
// environment only if a let-bound reference to the closure is used.
capt.println(i"boxing $rhs")
rhs.putAttachment(BoxedClosure, ())
case _ =>
case _ =>
super.recheckBlock(block, pt)

Expand Down Expand Up @@ -588,7 +586,7 @@ class CheckCaptures extends Recheck, SymTransformer:
* adding all references in the boxed capture set to the current environment.
*/
override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type =
if tree.isTerm && pt.isBoxedCapturing then
if tree.isTerm && (pt.isBoxedCapturing || tree.hasAttachment(BoxedClosure)) then
val saved = curEnv

tree match
Expand Down
10 changes: 10 additions & 0 deletions tests/pos-custom-args/captures/curried-closures.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import java.io.*
import annotation.capability

def Test4(g: OutputStream^) =
val xs: List[Int] = ???
val later = (f: OutputStream^) => (y: Int) => xs.foreach(x => f.write(x + y))
val _: (f: OutputStream^) ->{} Int ->{f} Unit = later

val later2 = () => (y: Int) => xs.foreach(x => g.write(x + y))
val _: () ->{} Int ->{g} Unit = later2

0 comments on commit 8030851

Please sign in to comment.