Skip to content

Commit

Permalink
Drop curried use scheme
Browse files Browse the repository at this point in the history
Drop the scheme where we only charge the last arrow of a curried lambda with
elements used in the body. On the one hand, this is unsound without compensation
measures (like, restricting to reach capabilities, or taking all capture sets
of a named curried function as the underlying reference). On the other hand, this
should be generalized to all closures and anonymous functions forming the right
hand sides of methods.
  • Loading branch information
odersky committed Oct 30, 2024
1 parent fc46e5c commit 9865f93
Show file tree
Hide file tree
Showing 9 changed files with 55 additions and 83 deletions.
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ object ccConfig:
*/
inline val handleEtaExpansionsSpecially = false

/** If enabled we drop inner uses in outer arrows of a curried function */
inline val DropOuterUsesInCurried = false

/** If true, use existential capture set variables */
def useExistentials(using Context) =
Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.5`)
Expand Down
9 changes: 6 additions & 3 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ object CheckCaptures:
case NestedInOwner // environment is a temporary one nested in the owner's environment,
// and does not have a different actual owner symbol
// (this happens when doing box adaptation).
case ClosureResult // environment is for the result of a closure
case ClosureResult // environment is for the result of a closure,
// used only under ccConfig.DropOuterUsesInCurried
case Boxed // environment is inside a box (in which case references are not counted)

/** A class describing environments.
Expand Down Expand Up @@ -180,7 +181,9 @@ object CheckCaptures:
if ccConfig.useSealed then check.traverse(tp)
end disallowRootCapabilitiesIn

/** Attachment key for bodies of closures, provided they are values */
/** Attachment key for bodies of closures, provided they are values.
* Used only under ccConfig.DropOuterUsesInCurried
*/
val ClosureBodyValue = Property.Key[Unit]

/** A prototype that indicates selection with an immutable value */
Expand Down Expand Up @@ -728,7 +731,7 @@ class CheckCaptures extends Recheck, SymTransformer:

override def recheckClosureBlock(mdef: DefDef, expr: Closure, pt: Type)(using Context): Type =
mdef.rhs match
case rhs @ closure(_, _, _) =>
case rhs @ closure(_, _, _) if ccConfig.DropOuterUsesInCurried =>
// 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
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
case _ => res
)
val fntpe = defn.PolyFunctionOf(mt)
if !encl.isEmpty && resDecomposed.isEmpty then
if !encl.isEmpty && (!ccConfig.DropOuterUsesInCurried || resDecomposed.isEmpty) then
val cs = CaptureSet(encl.map(_.paramRefs.head)*)
CapturingType(fntpe, cs, boxed = false)
else fntpe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ def Test4(g: OutputStream^) =
val _: (f: OutputStream^) ->{} Int ->{f} Unit = later

val later2 = () => (y: Int) => xs.foreach(x => g.write(x + y))
val _: () ->{} Int ->{g} Unit = later2
val _: () ->{} Int ->{g} Unit = later2 // error, inferred type is () ->{later2} Int ->{g} Unit

13 changes: 13 additions & 0 deletions tests/neg-custom-args/captures/i21620.check
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@
| A pure expression does nothing in statement position
|
| longer explanation available when compiling with `-explain`
-- [E129] Potential Issue Warning: tests/neg-custom-args/captures/i21620.scala:14:4 ------------------------------------
14 | x
| ^
| A pure expression does nothing in statement position
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21620.scala:9:31 ----------------------------------------
9 | val _: () -> () ->{x} Unit = f // error
| ^
| Found: () ->{f} () ->{x} Unit
| Required: () -> () ->{x} Unit
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21620.scala:20:33 ---------------------------------------
20 | val _: () ->{} () ->{x} Unit = f // error, but could be OK
| ^
| Found: () ->{f} () ->{x} Unit
| Required: () -> () ->{x} Unit
|
| longer explanation available when compiling with `-explain`
11 changes: 11 additions & 0 deletions tests/neg-custom-args/captures/i21620.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ def test(x: C^) =
() => foo()
val _: () -> () ->{x} Unit = f // error
()

def test2(x: C^) =
def foo() =
x
()
val f = () =>
// println() // uncomenting would give an error, but with
// a different way of handling curried functions should be OK
() => foo()
val _: () ->{} () ->{x} Unit = f // error, but could be OK
()
6 changes: 4 additions & 2 deletions tests/neg-custom-args/captures/leaked-curried.check
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
-- Error: tests/neg-custom-args/captures/leaked-curried.scala:14:20 ----------------------------------------------------
14 | () => () => io // error
| ^^
|(io : Cap^) cannot be referenced here; it is not included in the allowed capture set {} of the self type of class Fuzz
| (io : Cap^) cannot be referenced here; it is not included in the allowed capture set {}
| of an enclosing function literal with expected type () -> () ->{io} (ex$7: caps.Exists) -> Cap^{ex$7}
-- Error: tests/neg-custom-args/captures/leaked-curried.scala:17:20 ----------------------------------------------------
17 | () => () => io // error
| ^^
|(io : Cap^) cannot be referenced here; it is not included in the allowed capture set {} of the self type of class Foo
| (io : Cap^) cannot be referenced here; it is not included in the allowed capture set {}
| of an enclosing function literal with expected type () -> () ->{io} (ex$15: caps.Exists) -> Cap^{ex$15}
81 changes: 16 additions & 65 deletions tests/pos-custom-args/captures/i13816.scala
Original file line number Diff line number Diff line change
@@ -1,72 +1,23 @@
import language.experimental.saferExceptions
import language.experimental.erasedDefinitions

class Ex1 extends Exception("Ex1")
class Ex2 extends Exception("Ex2")
class Ex3 extends Exception("Ex3")

def foo0(i: Int): (CanThrow[Ex1], CanThrow[Ex2]) ?-> Unit =
if i > 0 then throw new Ex1 else throw new Ex2

/* Does not work yet curried dependent CFTs are not yet handled in typer
def foo01(i: Int): (ct: CanThrow[Ex1]) ?-> CanThrow[Ex2] ?->{ct} Unit =
if i > 0 then throw new Ex1 else throw new Ex2
*/

def foo1(i: Int): Unit throws Ex1 throws Ex2 =
if i > 0 then throw new Ex1 else throw new Ex1

def foo2(i: Int): Unit throws Ex1 | Ex2 =
if i > 0 then throw new Ex1 else throw new Ex2

def foo3(i: Int): Unit throws (Ex1 | Ex2) =
if i > 0 then throw new Ex1 else throw new Ex2

def foo4(i: Int)(using CanThrow[Ex1], CanThrow[Ex2]): Unit =
if i > 0 then throw new Ex1 else throw new Ex2

def foo5(i: Int)(using CanThrow[Ex1])(using CanThrow[Ex2]): Unit =
if i > 0 then throw new Ex1 else throw new Ex2

def foo6(i: Int)(using CanThrow[Ex1 | Ex2]): Unit =
if i > 0 then throw new Ex1 else throw new Ex2

def foo7(i: Int)(using CanThrow[Ex1]): Unit throws Ex1 | Ex2 =
if i > 0 then throw new Ex1 else throw new Ex2

/*
def foo8(i: Int)(using CanThrow[Ex2]): Unit throws Ex2 | Ex1 =
if i > 0 then throw new Ex1 else throw new Ex2

throw new Ex2
*/
def foo9(i: Int): Unit throws Ex1 | Ex2 | Ex3 =
if i > 0 then throw new Ex1
else if i < 0 then throw new Ex2
else throw new Ex3

def test(): Unit =
try
foo1(1)
foo2(1)
foo3(1)
foo4(1)
foo5(1)
foo6(1)
foo7(1)
foo8(1)
catch
case _: Ex1 =>
case _: Ex2 =>

try
try
foo1(1)
foo2(1)
foo3(1)
foo4(1)
foo5(1)
// foo6(1) // As explained in the docs this won't work until we find a way to aggregate capabilities
foo7(1)
foo8(1)
catch
case _: Ex1 =>
catch
case _: Ex2 =>
throw new Ex3
/*
def foo9a(i: Int):
(erased x$0: CanThrow[Ex3]^) ?-> (erased x$1: CanThrow[Ex2]^) ?->
(erased x$2: CanThrow[Ex1]^) ?->{x$1, x$0} Unit
=
(erased x3: CanThrow[Ex3])
?=> (erased x2: CanThrow[Ex2])
?=> (erased x1: CanThrow[Ex1])
?=> throw new Ex3
*/
11 changes: 0 additions & 11 deletions tests/pos-custom-args/captures/i21620.scala

This file was deleted.

0 comments on commit 9865f93

Please sign in to comment.