Skip to content

Commit c83a29e

Browse files
authored
Merge pull request #13914 from dotty-staging/multicatch-safer-exceptions
Better handling of multiple exceptions for saferExceptions
2 parents 33ab1b2 + 1eef69b commit c83a29e

File tree

8 files changed

+140
-65
lines changed

8 files changed

+140
-65
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,8 @@ object desugar {
12651265
* $throws[... $throws[A, E1] ... , En].
12661266
*/
12671267
def throws(tpt: Tree, op: Ident, excepts: Tree)(using Context): AppliedTypeTree = excepts match
1268+
case Parens(excepts1) =>
1269+
throws(tpt, op, excepts1)
12681270
case InfixOp(l, bar @ Ident(tpnme.raw.BAR), r) =>
12691271
throws(throws(tpt, op, l), bar, r)
12701272
case e =>

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,15 +1755,20 @@ class Typer extends Namer
17551755
untpd.ref(defn.Predef_undefined))
17561756
.withFlags(Given | Final | Lazy | Erased)
17571757
.withSpan(expr.span)
1758-
val caps =
1759-
for
1760-
case CaseDef(pat, guard, _) <- cases
1761-
if Feature.enabled(Feature.saferExceptions) && pat.tpe.widen.isCheckedException
1762-
yield
1763-
checkCatch(pat, guard)
1764-
makeCanThrow(pat.tpe.widen)
1765-
1766-
caps.foldLeft(expr)((e, g) => untpd.Block(g :: Nil, e))
1758+
val caughtExceptions =
1759+
if Feature.enabled(Feature.saferExceptions) then
1760+
for
1761+
CaseDef(pat, guard, _) <- cases
1762+
if pat.tpe.widen.isCheckedException
1763+
yield
1764+
checkCatch(pat, guard)
1765+
pat.tpe.widen
1766+
else Seq.empty
1767+
1768+
if caughtExceptions.isEmpty then expr
1769+
else
1770+
val capabilityProof = caughtExceptions.reduce(OrType(_, _, true))
1771+
untpd.Block(makeCanThrow(capabilityProof), expr)
17671772

17681773
def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = {
17691774
val expr2 :: cases2x = harmonic(harmonize, pt) {

docs/docs/reference/experimental/canthrow.md

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,27 @@ can alternatively be expressed like this:
8787
```scala
8888
def m(x: T): U throws E
8989
```
90-
Multiple `CanThrow` capabilities can be combined in a single throws clause. For instance, the method
90+
Also the capability to throw multiple types of exceptions can be expressed in a few ways as shown in the examples below:
9191
```scala
92-
def m2(x: T)(using CanThrow[E1], CanThrow[E2]): U
92+
def m(x: T): U throws E1 | E2
93+
def m(x: T): U throws E1 throws E2
94+
def m(x: T)(using CanThrow[E1], CanThrow[E2]): U
95+
def m(x: T)(using CanThrow[E1])(using CanThrow[E2]): U
96+
def m(x: T)(using CanThrow[E1]): U throws E2
9397
```
94-
can alternatively be expressed like this:
98+
99+
**Note 1:** A signature like
95100
```scala
96-
def m(x: T): U throws E1 | E2
101+
def m(x: T)(using CanThrow[E1 | E2]): U
97102
```
103+
would also allow throwing `E1` or `E2` inside the method's body but might cause problems when someone tried to call this method
104+
from another method declaring its `CanThrow` capabilities like in the earlier examples.
105+
This is because `CanThrow` has a contravariant type parameter so `CanThrow[E1 | E2]` is a subtype of both `CanThrow[E1]` and `CanThrow[E2]`.
106+
Hence the presence of a given instance of `CanThrow[E1 | E2]` in scope satisfies the requirement for `CanThrow[E1]` and `CanThrow[E2]`
107+
but given instances of `CanThrow[E1]` and `CanThrow[E2]` cannot be combined to provide and instance of `CanThrow[E1 | E2]`.
108+
109+
**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`.
110+
98111
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:
99112

100113
```scala
@@ -105,17 +118,29 @@ catch
105118
...
106119
case exN: ExN => handlerN
107120
```
108-
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:
121+
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:
109122
```scala
110123
try
111-
erased given CanThrow[Ex1] = ???
112-
...
113-
erased given CanThrow[ExN] = ???
124+
erased given CanThrow[Ex1 | ... | ExN] = ???
114125
body
115126
catch ...
116127
```
117-
Note that the right-hand side of all givens is `???` (undefined). This is OK since
118-
these givens are erased; they will not be executed at runtime.
128+
Note that the right-hand side of the synthesized given is `???` (undefined). This is OK since
129+
this given is erased; it will not be executed at runtime.
130+
131+
**Note 1:** The `saferExceptions` feature is designed to work only with checked exceptions. An exception type is _checked_ if it is a subtype of
132+
`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
133+
also may not refer to `RuntimeException`s.
134+
135+
**Note 2:** To keep things simple, the compiler will currently only generate capabilities
136+
for catch clauses of the form
137+
```scala
138+
case ex: Ex =>
139+
```
140+
where `ex` is an arbitrary variable name (`_` is also allowed), and `Ex` is an arbitrary
141+
checked exception type. Constructor patterns such as `Ex(...)` or patterns with guards
142+
are not allowed. The compiler will issue an error if one of these is used to catch
143+
a checked exception and `saferExceptions` is enabled.
119144

120145
## An Example
121146

@@ -133,17 +158,17 @@ def f(x: Double): Double =
133158
```
134159
You'll get this error message:
135160
```
136-
9 | if x < limit then x * x else throw LimitExceeded()
137-
| ^^^^^^^^^^^^^^^^^^^^^
138-
|The capability to throw exception LimitExceeded is missing.
139-
|The capability can be provided by one of the following:
140-
| - A using clause `(using CanThrow[LimitExceeded])`
141-
| - A `throws` clause in a result type such as `X throws LimitExceeded`
142-
| - an enclosing `try` that catches LimitExceeded
143-
|
144-
|The following import might fix the problem:
145-
|
146-
| import unsafeExceptions.canThrowAny
161+
if x < limit then x * x else throw LimitExceeded()
162+
^^^^^^^^^^^^^^^^^^^^^
163+
The capability to throw exception LimitExceeded is missing.
164+
The capability can be provided by one of the following:
165+
- Adding a using clause `(using CanThrow[LimitExceeded])` to the definition of the enclosing method
166+
- Adding `throws LimitExceeded` clause after the result type of the enclosing method
167+
- Wrapping this piece of code with a `try` block that catches LimitExceeded
168+
169+
The following import might fix the problem:
170+
171+
import unsafeExceptions.canThrowAny
147172
```
148173
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:
149174
```scala
@@ -179,20 +204,6 @@ closure may refer to capabilities in its free variables. This means that `map` i
179204
already effect polymorphic even though we did not change its signature at all.
180205
So the takeaway is that the effects as capabilities model naturally provides for effect polymorphism whereas this is something that other approaches struggle with.
181206

182-
**Note 1:** The compiler will only treat checked exceptions that way. An exception type is _checked_ if it is a subtype of
183-
`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
184-
also may not refer to `RuntimeException`s.
185-
186-
**Note 2:** To keep things simple, the compiler will currently only generate capabilities
187-
for catch clauses of the form
188-
```scala
189-
case ex: Ex =>
190-
```
191-
where `ex` is an arbitrary variable name (`_` is also allowed), and `Ex` is an arbitrary
192-
checked exception type. Constructor patterns such as `Ex(...)` or patterns with guards
193-
are not allowed. The compiler will issue an error if one of these is used to catch
194-
a checked exception and `saferExceptions` is enabled.
195-
196207
## Gradual Typing Via Imports
197208

198209
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

library/src/scala/CanThrow.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import annotation.{implicitNotFound, experimental}
77
* a given of class `CanThrow[Ex]` to be available.
88
*/
99
@experimental
10-
@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}")
10+
@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}")
1111
erased class CanThrow[-E <: Exception]
1212

1313
@experimental

tests/neg/i13846.check

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
-- Error: tests/neg/i13846.scala:7:9 -----------------------------------------------------------------------------------
66
7 | foo() // error
77
| ^
8-
| The capability to throw exception ArithmeticException is missing.
9-
| The capability can be provided by one of the following:
10-
| - A using clause `(using CanThrow[ArithmeticException])`
11-
| - A `throws` clause in a result type such as `X throws ArithmeticException`
12-
| - an enclosing `try` that catches ArithmeticException
8+
| The capability to throw exception ArithmeticException is missing.
9+
| The capability can be provided by one of the following:
10+
| - Adding a using clause `(using CanThrow[ArithmeticException])` to the definition of the enclosing method
11+
| - Adding `throws ArithmeticException` clause after the result type of the enclosing method
12+
| - Wrapping this piece of code with a `try` block that catches ArithmeticException
1313
|
14-
| The following import might fix the problem:
14+
| The following import might fix the problem:
1515
|
16-
| import unsafeExceptions.canThrowAny
16+
| import unsafeExceptions.canThrowAny
1717
|

tests/neg/i13864.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
| ^
99
| The capability to throw exception Ex[Int] is missing.
1010
| The capability can be provided by one of the following:
11-
| - A using clause `(using CanThrow[Ex[Int]])`
12-
| - A `throws` clause in a result type such as `X throws Ex[Int]`
13-
| - an enclosing `try` that catches Ex[Int]
11+
| - Adding a using clause `(using CanThrow[Ex[Int]])` to the definition of the enclosing method
12+
| - Adding `throws Ex[Int]` clause after the result type of the enclosing method
13+
| - Wrapping this piece of code with a `try` block that catches Ex[Int]
1414
|
1515
| The following import might fix the problem:
1616
|

tests/neg/saferExceptions.check

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
| ^^^^^^^^^^^^^^^^^
44
| The capability to throw exception Exception is missing.
55
| The capability can be provided by one of the following:
6-
| - A using clause `(using CanThrow[Exception])`
7-
| - A `throws` clause in a result type such as `X throws Exception`
8-
| - an enclosing `try` that catches Exception
6+
| - Adding a using clause `(using CanThrow[Exception])` to the definition of the enclosing method
7+
| - Adding `throws Exception` clause after the result type of the enclosing method
8+
| - Wrapping this piece of code with a `try` block that catches Exception
99
|
1010
| The following import might fix the problem:
1111
|
@@ -14,13 +14,13 @@
1414
-- Error: tests/neg/saferExceptions.scala:17:46 ------------------------------------------------------------------------
1515
17 | def baz(x: Int): Int throws Failure = bar(x) // error
1616
| ^
17-
| The capability to throw exception java.io.IOException is missing.
18-
| The capability can be provided by one of the following:
19-
| - A using clause `(using CanThrow[java.io.IOException])`
20-
| - A `throws` clause in a result type such as `X throws java.io.IOException`
21-
| - an enclosing `try` that catches java.io.IOException
17+
| The capability to throw exception java.io.IOException is missing.
18+
| The capability can be provided by one of the following:
19+
| - Adding a using clause `(using CanThrow[java.io.IOException])` to the definition of the enclosing method
20+
| - Adding `throws java.io.IOException` clause after the result type of the enclosing method
21+
| - Wrapping this piece of code with a `try` block that catches java.io.IOException
2222
|
23-
| The following import might fix the problem:
23+
| The following import might fix the problem:
2424
|
25-
| import unsafeExceptions.canThrowAny
25+
| import unsafeExceptions.canThrowAny
2626
|

tests/pos/i13816.scala

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import language.experimental.saferExceptions
2+
3+
class Ex1 extends Exception("Ex1")
4+
class Ex2 extends Exception("Ex2")
5+
6+
def foo1(i: Int): Unit throws Ex1 throws Ex2 =
7+
if i > 0 then throw new Ex1 else throw new Ex2
8+
9+
def foo2(i: Int): Unit throws Ex1 | Ex2 =
10+
if i > 0 then throw new Ex1 else throw new Ex2
11+
12+
def foo3(i: Int): Unit throws (Ex1 | Ex2) =
13+
if i > 0 then throw new Ex1 else throw new Ex2
14+
15+
def foo4(i: Int)(using CanThrow[Ex1], CanThrow[Ex2]): Unit =
16+
if i > 0 then throw new Ex1 else throw new Ex2
17+
18+
def foo5(i: Int)(using CanThrow[Ex1])(using CanThrow[Ex2]): Unit =
19+
if i > 0 then throw new Ex1 else throw new Ex2
20+
21+
def foo6(i: Int)(using CanThrow[Ex1 | Ex2]): Unit =
22+
if i > 0 then throw new Ex1 else throw new Ex2
23+
24+
def foo7(i: Int)(using CanThrow[Ex1]): Unit throws Ex2 =
25+
if i > 0 then throw new Ex1 else throw new Ex2
26+
27+
def foo8(i: Int)(using CanThrow[Ex2]): Unit throws Ex1 =
28+
if i > 0 then throw new Ex1 else throw new Ex2
29+
30+
def test(): Unit =
31+
try
32+
foo1(1)
33+
foo2(1)
34+
foo3(1)
35+
foo4(1)
36+
foo5(1)
37+
foo6(1)
38+
foo7(1)
39+
foo8(1)
40+
catch
41+
case _: Ex1 =>
42+
case _: Ex2 =>
43+
44+
try
45+
try
46+
foo1(1)
47+
foo2(1)
48+
foo3(1)
49+
foo4(1)
50+
foo5(1)
51+
// foo6(1) // As explained in the docs this won't work until we find a way to aggregate capabilities
52+
foo7(1)
53+
foo8(1)
54+
catch
55+
case _: Ex1 =>
56+
catch
57+
case _: Ex2 =>

0 commit comments

Comments
 (0)