Skip to content

Commit 05114db

Browse files
committed
Better handling of multiple exceptions for saferExceptions
* Generate a single accumulated CanThrow capability for multiple catch cases in a try * Allow parentheses around exceptions alternatives in throws clauses
1 parent e23f815 commit 05114db

File tree

4 files changed

+106
-44
lines changed

4 files changed

+106
-44
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: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,15 +1749,22 @@ class Typer extends Namer
17491749
untpd.ref(defn.Predef_undefined))
17501750
.withFlags(Given | Final | Lazy | Erased)
17511751
.withSpan(expr.span)
1752-
val caps =
1753-
for
1754-
case CaseDef(pat, guard, _) <- cases
1755-
if Feature.enabled(Feature.saferExceptions) && pat.tpe.widen.isCheckedException
1756-
yield
1757-
checkCatch(pat, guard)
1758-
makeCanThrow(pat.tpe.widen)
1759-
1760-
caps.foldLeft(expr)((e, g) => untpd.Block(g :: Nil, e))
1752+
val caughtExceptions =
1753+
if Feature.enabled(Feature.saferExceptions) then
1754+
for
1755+
CaseDef(pat, guard, _) <- cases
1756+
tpe = pat.tpe.widen
1757+
if tpe.isCheckedException
1758+
yield
1759+
checkCatch(pat, guard)
1760+
tpe
1761+
else Seq.empty
1762+
1763+
caughtExceptions match
1764+
case Nil => expr
1765+
case head :: tail =>
1766+
val capabilityProof = tail.foldLeft(head: Type)(OrType(_, _, true))
1767+
untpd.Block(makeCanThrow(capabilityProof), expr)
17611768

17621769
def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = {
17631770
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+
- A using clause `(using CanThrow[LimitExceeded])`
166+
- A `throws` clause in a result type such as `X throws LimitExceeded`
167+
- an enclosing `try` 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

tests/pos/i13816.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 =>

0 commit comments

Comments
 (0)