diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index b652a49c0381..7121f7e7370d 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1265,6 +1265,8 @@ object desugar { * $throws[... $throws[A, E1] ... , En]. */ def throws(tpt: Tree, op: Ident, excepts: Tree)(using Context): AppliedTypeTree = excepts match + case Parens(excepts1) => + throws(tpt, op, excepts1) case InfixOp(l, bar @ Ident(tpnme.raw.BAR), r) => throws(throws(tpt, op, l), bar, r) case e => diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 4c2533d6dbba..3afa6aade710 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1749,15 +1749,20 @@ class Typer extends Namer untpd.ref(defn.Predef_undefined)) .withFlags(Given | Final | Lazy | Erased) .withSpan(expr.span) - val caps = - for - case CaseDef(pat, guard, _) <- cases - if Feature.enabled(Feature.saferExceptions) && pat.tpe.widen.isCheckedException - yield - checkCatch(pat, guard) - makeCanThrow(pat.tpe.widen) - - caps.foldLeft(expr)((e, g) => untpd.Block(g :: Nil, e)) + val caughtExceptions = + if Feature.enabled(Feature.saferExceptions) then + for + CaseDef(pat, guard, _) <- cases + if pat.tpe.widen.isCheckedException + yield + checkCatch(pat, guard) + pat.tpe.widen + else Seq.empty + + if caughtExceptions.isEmpty then expr + else + val capabilityProof = caughtExceptions.reduce(OrType(_, _, true)) + untpd.Block(makeCanThrow(capabilityProof), expr) def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = { val expr2 :: cases2x = harmonic(harmonize, pt) { diff --git a/docs/docs/reference/experimental/canthrow.md b/docs/docs/reference/experimental/canthrow.md index 6377a040b515..df92676831d2 100644 --- a/docs/docs/reference/experimental/canthrow.md +++ b/docs/docs/reference/experimental/canthrow.md @@ -87,14 +87,27 @@ can alternatively be expressed like this: ```scala def m(x: T): U throws E ``` -Multiple `CanThrow` capabilities can be combined in a single throws clause. For instance, the method +Also the capability to throw multiple types of exceptions can be expressed in a few ways as shown in the examples below: ```scala -def m2(x: T)(using CanThrow[E1], CanThrow[E2]): U +def m(x: T): U throws E1 | E2 +def m(x: T): U throws E1 throws E2 +def m(x: T)(using CanThrow[E1], CanThrow[E2]): U +def m(x: T)(using CanThrow[E1])(using CanThrow[E2]): U +def m(x: T)(using CanThrow[E1]): U throws E2 ``` -can alternatively be expressed like this: + +**Note 1:** A signature like ```scala -def m(x: T): U throws E1 | E2 +def m(x: T)(using CanThrow[E1 | E2]): U ``` +would also allow throwing `E1` or `E2` inside the method's body but might cause problems when someone tried to call this method +from another method declaring its `CanThrow` capabilities like in the earlier examples. +This is because `CanThrow` has a contravariant type parameter so `CanThrow[E1 | E2]` is a subtype of both `CanThrow[E1]` and `CanThrow[E2]`. +Hence the presence of a given instance of `CanThrow[E1 | E2]` in scope satisfies the requirement for `CanThrow[E1]` and `CanThrow[E2]` +but given instances of `CanThrow[E1]` and `CanThrow[E2]` cannot be combined to provide and instance of `CanThrow[E1 | E2]`. + +**Note 2:** One should keep in mind that `|` binds its left and right arguments more tightly than `throws` so `A | B throws E1 | E2` means `(A | B) throws (Ex1 | Ex2)`, not `A | (B throws E1) | E2`. + The `CanThrow`/`throws` combo essentially propagates the `CanThrow` requirement outwards. But where are these capabilities created in the first place? That's in the `try` expression. Given a `try` like this: ```scala @@ -105,17 +118,29 @@ catch ... case exN: ExN => handlerN ``` -the compiler generates capabilities for `CanThrow[Ex1]`, ..., `CanThrow[ExN]` that are in scope as givens in `body`. It does this by augmenting the `try` roughly as follows: +the compiler generates an accumulated capability of type `CanThrow[Ex1 | ... | Ex2]` that is available as a given in the scope of `body`. It does this by augmenting the `try` roughly as follows: ```scala try - erased given CanThrow[Ex1] = ??? - ... - erased given CanThrow[ExN] = ??? + erased given CanThrow[Ex1 | ... | ExN] = ??? body catch ... ``` -Note that the right-hand side of all givens is `???` (undefined). This is OK since -these givens are erased; they will not be executed at runtime. +Note that the right-hand side of the synthesized given is `???` (undefined). This is OK since +this given is erased; it will not be executed at runtime. + +**Note 1:** The `saferExceptions` feature is designed to work only with checked exceptions. An exception type is _checked_ if it is a subtype of +`Exception` but not of `RuntimeException`. The signature of `CanThrow` still admits `RuntimeException`s since `RuntimeException` is a proper subtype of its bound, `Exception`. But no capabilities will be generated for `RuntimeException`s. Furthermore, `throws` clauses +also may not refer to `RuntimeException`s. + +**Note 2:** To keep things simple, the compiler will currently only generate capabilities +for catch clauses of the form +```scala + case ex: Ex => +``` +where `ex` is an arbitrary variable name (`_` is also allowed), and `Ex` is an arbitrary +checked exception type. Constructor patterns such as `Ex(...)` or patterns with guards +are not allowed. The compiler will issue an error if one of these is used to catch +a checked exception and `saferExceptions` is enabled. ## An Example @@ -133,17 +158,17 @@ def f(x: Double): Double = ``` You'll get this error message: ``` -9 | if x < limit then x * x else throw LimitExceeded() - | ^^^^^^^^^^^^^^^^^^^^^ - |The capability to throw exception LimitExceeded is missing. - |The capability can be provided by one of the following: - | - A using clause `(using CanThrow[LimitExceeded])` - | - A `throws` clause in a result type such as `X throws LimitExceeded` - | - an enclosing `try` that catches LimitExceeded - | - |The following import might fix the problem: - | - | import unsafeExceptions.canThrowAny + if x < limit then x * x else throw LimitExceeded() + ^^^^^^^^^^^^^^^^^^^^^ +The capability to throw exception LimitExceeded is missing. +The capability can be provided by one of the following: + - Adding a using clause `(using CanThrow[LimitExceeded])` to the definition of the enclosing method + - Adding `throws LimitExceeded` clause after the result type of the enclosing method + - Wrapping this piece of code with a `try` block that catches LimitExceeded + +The following import might fix the problem: + + import unsafeExceptions.canThrowAny ``` As the error message implies, you have to declare that `f` needs the capability to throw a `LimitExceeded` exception. The most concise way to do so is to add a `throws` clause: ```scala @@ -179,20 +204,6 @@ closure may refer to capabilities in its free variables. This means that `map` i already effect polymorphic even though we did not change its signature at all. So the takeaway is that the effects as capabilities model naturally provides for effect polymorphism whereas this is something that other approaches struggle with. -**Note 1:** The compiler will only treat checked exceptions that way. An exception type is _checked_ if it is a subtype of -`Exception` but not of `RuntimeException`. The signature of `CanThrow` still admits `RuntimeException`s since `RuntimeException` is a proper subtype of its bound, `Exception`. But no capabilities will be generated for `RuntimeException`s. Furthermore, `throws` clauses -also may not refer to `RuntimeException`s. - -**Note 2:** To keep things simple, the compiler will currently only generate capabilities -for catch clauses of the form -```scala - case ex: Ex => -``` -where `ex` is an arbitrary variable name (`_` is also allowed), and `Ex` is an arbitrary -checked exception type. Constructor patterns such as `Ex(...)` or patterns with guards -are not allowed. The compiler will issue an error if one of these is used to catch -a checked exception and `saferExceptions` is enabled. - ## Gradual Typing Via Imports Another advantage is that the model allows a gradual migration from current unchecked exceptions to safer exceptions. Imagine for a moment that `experimental.saferExceptions` is turned on everywhere. There would be lots of code that breaks since functions have not yet been properly annotated with `throws`. But it's easy to create an escape hatch that lets us ignore the breakages for a while: simply add the import diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index 4da00b8a76ac..3e66e42514e4 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -7,7 +7,7 @@ import annotation.{implicitNotFound, experimental} * a given of class `CanThrow[Ex]` to be available. */ @experimental -@implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - A using clause `(using CanThrow[${E}])`\n - A `throws` clause in a result type such as `X throws ${E}`\n - an enclosing `try` that catches ${E}") +@implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") erased class CanThrow[-E <: Exception] @experimental diff --git a/tests/neg/i13846.check b/tests/neg/i13846.check index 50de19874b8c..69ea0f0e51ac 100644 --- a/tests/neg/i13846.check +++ b/tests/neg/i13846.check @@ -5,13 +5,13 @@ -- Error: tests/neg/i13846.scala:7:9 ----------------------------------------------------------------------------------- 7 | foo() // error | ^ - | The capability to throw exception ArithmeticException is missing. - | The capability can be provided by one of the following: - | - A using clause `(using CanThrow[ArithmeticException])` - | - A `throws` clause in a result type such as `X throws ArithmeticException` - | - an enclosing `try` that catches ArithmeticException + | The capability to throw exception ArithmeticException is missing. + | The capability can be provided by one of the following: + | - Adding a using clause `(using CanThrow[ArithmeticException])` to the definition of the enclosing method + | - Adding `throws ArithmeticException` clause after the result type of the enclosing method + | - Wrapping this piece of code with a `try` block that catches ArithmeticException | - | The following import might fix the problem: + | The following import might fix the problem: | - | import unsafeExceptions.canThrowAny + | import unsafeExceptions.canThrowAny | diff --git a/tests/neg/i13864.check b/tests/neg/i13864.check index bce0788d31ce..54e81ea82774 100644 --- a/tests/neg/i13864.check +++ b/tests/neg/i13864.check @@ -8,9 +8,9 @@ | ^ | The capability to throw exception Ex[Int] is missing. | The capability can be provided by one of the following: - | - A using clause `(using CanThrow[Ex[Int]])` - | - A `throws` clause in a result type such as `X throws Ex[Int]` - | - an enclosing `try` that catches Ex[Int] + | - Adding a using clause `(using CanThrow[Ex[Int]])` to the definition of the enclosing method + | - Adding `throws Ex[Int]` clause after the result type of the enclosing method + | - Wrapping this piece of code with a `try` block that catches Ex[Int] | | The following import might fix the problem: | diff --git a/tests/neg/saferExceptions.check b/tests/neg/saferExceptions.check index 06c5bcc1a547..5f51ce08d6db 100644 --- a/tests/neg/saferExceptions.check +++ b/tests/neg/saferExceptions.check @@ -3,9 +3,9 @@ | ^^^^^^^^^^^^^^^^^ | The capability to throw exception Exception is missing. | The capability can be provided by one of the following: - | - A using clause `(using CanThrow[Exception])` - | - A `throws` clause in a result type such as `X throws Exception` - | - an enclosing `try` that catches Exception + | - Adding a using clause `(using CanThrow[Exception])` to the definition of the enclosing method + | - Adding `throws Exception` clause after the result type of the enclosing method + | - Wrapping this piece of code with a `try` block that catches Exception | | The following import might fix the problem: | @@ -14,13 +14,13 @@ -- Error: tests/neg/saferExceptions.scala:17:46 ------------------------------------------------------------------------ 17 | def baz(x: Int): Int throws Failure = bar(x) // error | ^ - | The capability to throw exception java.io.IOException is missing. - | The capability can be provided by one of the following: - | - A using clause `(using CanThrow[java.io.IOException])` - | - A `throws` clause in a result type such as `X throws java.io.IOException` - | - an enclosing `try` that catches java.io.IOException + | The capability to throw exception java.io.IOException is missing. + | The capability can be provided by one of the following: + | - Adding a using clause `(using CanThrow[java.io.IOException])` to the definition of the enclosing method + | - Adding `throws java.io.IOException` clause after the result type of the enclosing method + | - Wrapping this piece of code with a `try` block that catches java.io.IOException | - | The following import might fix the problem: + | The following import might fix the problem: | - | import unsafeExceptions.canThrowAny + | import unsafeExceptions.canThrowAny | diff --git a/tests/pos/i13816.scala b/tests/pos/i13816.scala new file mode 100644 index 000000000000..a87be4803936 --- /dev/null +++ b/tests/pos/i13816.scala @@ -0,0 +1,57 @@ +import language.experimental.saferExceptions + +class Ex1 extends Exception("Ex1") +class Ex2 extends Exception("Ex2") + +def foo1(i: Int): Unit throws Ex1 throws Ex2 = + if i > 0 then throw new Ex1 else throw new Ex2 + +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 Ex2 = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo8(i: Int)(using CanThrow[Ex2]): Unit throws Ex1 = + if i > 0 then throw new Ex1 else throw new Ex2 + +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 =>