Skip to content

Commit 6285368

Browse files
committed
Use soft keyword throws instead of canThrow
1 parent 94ec9bb commit 6285368

15 files changed

+85
-58
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,14 @@ object desugar {
12611261
makeOp(left, right, Span(left.span.start, op.span.end, op.span.start))
12621262
}
12631263

1264+
/** Translate throws type `A throws E1 | ... | En`
1265+
*/
1266+
def throws(tpt: Tree, op: Ident, excepts: Tree)(using Context): AppliedTypeTree = excepts match
1267+
case InfixOp(l, bar @ Ident(tpnme.raw.BAR), r) =>
1268+
throws(throws(tpt, op, l), bar, r)
1269+
case e =>
1270+
AppliedTypeTree(cpy.Ident(op)(tpnme.THROWS), tpt :: excepts :: Nil)
1271+
12641272
/** Translate tuple expressions of arity <= 22
12651273
*
12661274
* () ==> ()

compiler/src/dotty/tools/dotc/core/StdNames.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ object StdNames {
305305
val SPECIALIZED_INSTANCE: N = "specInstance$"
306306
val THIS: N = "_$this"
307307
val TRAIT_CONSTRUCTOR: N = "$init$"
308+
val THROWS: N = "$throws"
308309
val U2EVT: N = "u2evt$"
309310
val ALLARGS: N = "$allArgs"
310311

@@ -600,6 +601,7 @@ object StdNames {
600601
val this_ : N = "this"
601602
val thisPrefix : N = "thisPrefix"
602603
val throw_ : N = "throw"
604+
val throws: N = "throws"
603605
val toArray: N = "toArray"
604606
val toList: N = "toList"
605607
val toObjectArray : N = "toObjectArray"

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2600,7 +2600,10 @@ class Typer extends Namer
26002600
val untpd.InfixOp(l, op, r) = tree
26012601
val result =
26022602
if (ctx.mode.is(Mode.Type))
2603-
typedAppliedTypeTree(cpy.AppliedTypeTree(tree)(op, l :: r :: Nil))
2603+
typedAppliedTypeTree(
2604+
if op.name == tpnme.throws && Feature.enabled(Feature.saferExceptions)
2605+
then desugar.throws(l, op, r)
2606+
else cpy.AppliedTypeTree(tree)(op, l :: r :: Nil))
26042607
else if (ctx.mode.is(Mode.Pattern))
26052608
typedUnApply(cpy.Apply(tree)(op, l :: r :: Nil), pt)
26062609
else {

docs/docs/reference/experimental/canthrow.md

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ However, a programming language is not a framework; it has to cater also for tho
4747
Why does `map` work so poorly with Java's checked exception model? It's because
4848
`map`'s signature limits function arguments to not throw checked exceptions. We could try to come up with a more polymorphic formulation of `map`. For instance, it could look like this:
4949
```scala
50-
def map[B, E](f: A => B canThrow E): List[B] canThrow E
50+
def map[B, E](f: A => B throws E): List[B] throws E
5151
```
52-
This assumes a type `A canThrow E` to indicate computations of type `A` that can throw an exception of type `E`. But in practice the overhead of the additional type parameters makes this approach unappealing as well. Note in particular that we'd have to parameterize _every method_ that takes a function argument that way, so the added overhead of declaring all these exception types looks just like a sort of ceremony we would like to avoid.
52+
This assumes a type `A throws E` to indicate computations of type `A` that can throw an exception of type `E`. But in practice the overhead of the additional type parameters makes this approach unappealing as well. Note in particular that we'd have to parameterize _every method_ that takes a function argument that way, so the added overhead of declaring all these exception types looks just like a sort of ceremony we would like to avoid.
5353

5454
But there is a way to avoid the ceremony. Instead of concentrating on possible _effects_ such as "this code might throw an exception", concentrate on _capabilities_ such as "this code needs the capability to throw an exception". From a standpoint of expressiveness this is quite similar. But capabilities can be expressed as parameters whereas traditionally effects are expressed as some addition to result values. It turns out that this can make a big difference!
5555

@@ -71,19 +71,32 @@ How can the ability be produced? There are several possibilities:
7171
Most often, the ability is produced by having a using clause `(using CanThrow[Exc])` in some enclosing scope. This roughly corresponds to a `throws` clause
7272
in Java. The analogy is even stronger since alongside `CanThrow` there is also the following type alias defined in the `scala` package:
7373
```scala
74-
infix type canThrow[R, +E <: Exception] = CanThrow[E] ?=> R
74+
infix type $throws[R, +E <: Exception] = CanThrow[E] ?=> R
7575
```
76-
That is, `R canThrow E` is a context function type that takes an implicit `CanThrow[E]` parameter and that returns a value of type `R`. Therefore, a method written like this:
76+
That is, `R $throws E` is a context function type that takes an implicit `CanThrow[E]` parameter and that returns a value of type `R`. What's more, the compiler
77+
will translate an infix types with `throws` as the operator to `$throws` applications
78+
according to the rules
79+
```
80+
A throws E --> A $throws E
81+
A throws E₁ | ... | Eᵢ --> A $throws E₁ ... $throws Eᵢ
82+
```
83+
Therefore, a method written like this:
7784
```scala
7885
def m(x: T)(using CanThrow[E]): U
7986
```
8087
can alternatively be expressed like this:
8188
```scala
82-
def m(x: T): U canThrow E
89+
def m(x: T): U throws E
8390
```
84-
_Aside_: If we rename `canThrow` to `throws` we would have a perfect analogy with Java but unfortunately `throws` is already taken in Scala 2.13.
85-
86-
The `CanThrow`/`canThrow` combo essentially propagates the `CanThrow` requirement outwards. But where are these abilities created in the first place? That's in the `try` expression. Given a `try` like this:
91+
Multiple `CanThrow` capabilities can be combined in a single throws clause. For instance, the method
92+
```scala
93+
def m2(x: T)(using CanThrow[E1], CanThrow[E2]): U
94+
```
95+
can alternatively be expressed like this:
96+
```scala
97+
def m(x: T): U throws E1 | E2
98+
```
99+
The `CanThrow`/`throws` combo essentially propagates the `CanThrow` requirement outwards. But where are these abilities created in the first place? That's in the `try` expression. Given a `try` like this:
87100

88101
```scala
89102
try
@@ -126,16 +139,16 @@ You'll get this error message:
126139
|The ability to throw exception LimitExceeded is missing.
127140
|The ability can be provided by one of the following:
128141
| - A using clause `(using CanThrow[LimitExceeded])`
129-
| - A `canThrow` clause in a result type such as `X canThrow LimitExceeded`
142+
| - A `throws` clause in a result type such as `X throws LimitExceeded`
130143
| - an enclosing `try` that catches LimitExceeded
131144
|
132145
|The following import might fix the problem:
133146
|
134147
| import unsafeExceptions.canThrowAny
135148
```
136-
As the error message implies, you have to declare that `f` needs the ability to throw a `LimitExceeded` exception. The most concise way to do so is to add a `canThrow` clause:
149+
As the error message implies, you have to declare that `f` needs the ability to throw a `LimitExceeded` exception. The most concise way to do so is to add a `throws` clause:
137150
```scala
138-
def f(x: Double): Double canThrow LimitExceeded =
151+
def f(x: Double): Double throws LimitExceeded =
139152
if x < limit then x * x else throw LimitExceeded()
140153
```
141154
Now put a call to `f` in a `try` that catches `LimitExceeded`:
@@ -169,12 +182,12 @@ So the takeaway is that the effects as abilities model naturally provides for ef
169182

170183
## Gradual Typing Via Imports
171184

172-
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 `canThrow`. But it's easy to create an escape hatch that lets us ignore the breakages for a while: simply add the import
185+
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
173186
```scala
174187
import scala.unsafeExceptions.canThrowAny
175188
```
176189
This will provide the `CanThrow` ability for any exception, and thereby allow
177-
all throws and all other calls, no matter what the current state of `canThrow` declarations is. Here's the
190+
all throws and all other calls, no matter what the current state of `throws` declarations is. Here's the
178191
definition of `canThrowAny`:
179192
```scala
180193
package scala
@@ -188,7 +201,8 @@ enable more fluid explorations of code without regard for complete exception saf
188201

189202
To summarize, the extension for safer exception checking consists of the following elements:
190203

191-
- It adds to the standard library the class `scala.CanThrow`, the type `scala.canThrow`, and the `scala.unsafeExceptions` object, as they were described above.
204+
- It adds to the standard library the class `scala.CanThrow`, the type `scala.$throws`, and the `scala.unsafeExceptions` object, as they were described above.
205+
- It adds some desugaring rules ro rewrite `throws` types to cascaded `$throws` types.
192206
- It augments the type checking of `throw` by _demanding_ a `CanThrow` ability or the thrown exception.
193207
- It augments the type checking of `try` by _providing_ `CanThrow` abilities for every caught exception.
194208

docs/docs/reference/soft-modifier.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: Soft Keywords
55

66
A soft modifier is one of the identifiers `opaque`, `inline`, `open`, `transparent`, and `infix`.
77

8-
A soft keyword is a soft modifier, or one of `derives`, `end`, `extension`, `using`, `|`, `+`, `-`, `*`
8+
A soft keyword is a soft modifier, or one of `derives`, `end`, `extension`, `throws`, `using`, `|`, `+`, `-`, `*`
99

1010
A soft modifier is treated as potential modifier of a definition if it is followed by a hard modifier or a keyword combination starting a definition (`def`, `val`, `var`, `type`, `given`, `class`, `trait`, `object`, `enum`, `case class`, `case object`). Between the two words there may be a sequence of newline tokens and soft modifiers.
1111

library/src-bootstrapped/scala/CanThrow.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ import annotation.{implicitNotFound, experimental}
77
* a given of class `CanThrow[Ex]` to be available.
88
*/
99
@experimental
10-
@implicitNotFound("The ability to throw exception ${E} is missing.\nThe ability can be provided by one of the following:\n - A using clause `(using CanThrow[${E}])`\n - A `canThrow` clause in a result type such as `X canThrow ${E}`\n - an enclosing `try` that catches ${E}")
10+
@implicitNotFound("The ability to throw exception ${E} is missing.\nThe ability 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}")
1111
erased class CanThrow[-E <: Exception]
1212

1313
/** A helper type to allow syntax like
1414
*
15-
* def f(): T canThrow Ex
15+
* def f(): T throws Ex1 | Ex2
16+
*
17+
* Used in desugar.throws.
1618
*/
1719
@experimental
18-
infix type canThrow[R, +E <: Exception] = CanThrow[E] ?=> R
20+
infix type $throws[R, +E <: Exception] = CanThrow[E] ?=> R
1921

2022
@experimental
2123
object unsafeExceptions:

tests/neg/safeThrowsStrawman.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
| The capability to throw exception scalax.Fail is missing.
55
| The capability can be provided by one of the following:
66
| - A using clause `(using CanThrow[scalax.Fail])`
7-
| - A throws clause in a result type such as `X throws scalax.Fail`
7+
| - A raises clause in a result type such as `X raises scalax.Fail`
88
| - an enclosing `try` that catches scalax.Fail
99
-- Error: tests/neg/safeThrowsStrawman.scala:27:15 ---------------------------------------------------------------------
1010
27 | println(bar) // error
1111
| ^
1212
| The capability to throw exception Exception is missing.
1313
| The capability can be provided by one of the following:
1414
| - A using clause `(using CanThrow[Exception])`
15-
| - A throws clause in a result type such as `X throws Exception`
15+
| - A raises clause in a result type such as `X raises Exception`
1616
| - an enclosing `try` that catches Exception

tests/neg/safeThrowsStrawman.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@ import language.experimental.erasedDefinitions
22
import annotation.implicitNotFound
33

44
object scalax:
5-
@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}")
5+
@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 raises clause in a result type such as `X raises ${E}`\n - an enclosing `try` that catches ${E}")
66
erased class CanThrow[-E <: Exception]
77

8-
infix type throws[R, +E <: Exception] = CanThrow[E] ?=> R
8+
infix type raises[R, +E <: Exception] = CanThrow[E] ?=> R
99

1010
class Fail extends Exception
1111

12-
def raise[E <: Exception](e: E): Nothing throws E = throw e
12+
def raise[E <: Exception](e: E): Nothing raises E = throw e
1313

1414
import scalax._
1515

1616
def foo(x: Boolean): Int =
1717
if x then 1 else raise(Fail()) // error
1818

19-
def bar: Int throws Exception =
19+
def bar: Int raises Exception =
2020
raise(Fail())
2121

2222
@main def Test =

tests/neg/safeThrowsStrawman2.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ object scalax:
44
erased class CanThrow[E <: Exception]
55
type CTF = CanThrow[Fail]
66

7-
infix type throws[R, E <: Exception] = CanThrow[E] ?=> R
7+
infix type raises[R, E <: Exception] = CanThrow[E] ?=> R
88

99
class Fail extends Exception
1010

11-
def raise[E <: Exception](e: E): Nothing throws E = throw e
11+
def raise[E <: Exception](e: E): Nothing raises E = throw e
1212

1313
import scalax._
1414

15-
def foo(x: Boolean, y: CanThrow[Fail]): Int throws Fail =
15+
def foo(x: Boolean, y: CanThrow[Fail]): Int raises Fail =
1616
if x then 1 else raise(Fail())
1717

1818
def bar(x: Boolean)(using CanThrow[Fail]): Int =

tests/neg/saferExceptions.check

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
-- Error: tests/neg/saferExceptions.scala:14:16 ------------------------------------------------------------------------
2-
14 | case 4 => throw Exception() // error
1+
-- Error: tests/neg/saferExceptions.scala:12:16 ------------------------------------------------------------------------
2+
12 | case 4 => throw Exception() // error
33
| ^^^^^^^^^^^^^^^^^
44
| The ability to throw exception Exception is missing.
55
| The ability can be provided by one of the following:
66
| - A using clause `(using CanThrow[Exception])`
7-
| - A `canThrow` clause in a result type such as `X canThrow Exception`
7+
| - A `throws` clause in a result type such as `X throws Exception`
88
| - an enclosing `try` that catches Exception
99
|
1010
| The following import might fix the problem:
1111
|
1212
| import unsafeExceptions.canThrowAny
1313
|
14-
-- Error: tests/neg/saferExceptions.scala:19:48 ------------------------------------------------------------------------
15-
19 | def baz(x: Int): Int canThrow Failure = bar(x) // error
16-
| ^
17-
| The ability to throw exception java.io.IOException is missing.
18-
| The ability can be provided by one of the following:
19-
| - A using clause `(using CanThrow[java.io.IOException])`
20-
| - A `canThrow` clause in a result type such as `X canThrow java.io.IOException`
21-
| - an enclosing `try` that catches java.io.IOException
14+
-- Error: tests/neg/saferExceptions.scala:17:46 ------------------------------------------------------------------------
15+
17 | def baz(x: Int): Int throws Failure = bar(x) // error
16+
| ^
17+
| The ability to throw exception java.io.IOException is missing.
18+
| The ability 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
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/neg/saferExceptions.scala

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ object test:
44

55
class Failure extends Exception
66

7-
def bar(x: Int): Int
8-
`canThrow` Failure
9-
`canThrow` IOException =
7+
def bar(x: Int): Int throws Failure | IOException =
108
x match
119
case 1 => throw AssertionError()
1210
case 2 => throw Failure() // ok
@@ -15,5 +13,5 @@ object test:
1513
case 5 => throw Throwable() // ok: Throwable is treated as unchecked
1614
case _ => 0
1715

18-
def foo(x: Int): Int canThrow Exception = bar(x)
19-
def baz(x: Int): Int canThrow Failure = bar(x) // error
16+
def foo(x: Int): Int throws Exception = bar(x)
17+
def baz(x: Int): Int throws Failure = bar(x) // error

tests/pos/reference/saferExceptions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class LimitExceeded extends Exception
55

66
val limit = 10e9
77

8-
def f(x: Double): Double canThrow LimitExceeded =
8+
def f(x: Double): Double throws LimitExceeded =
99
if x < limit then x * x else throw LimitExceeded()
1010

1111
@main def test(xs: Double*) =

tests/run/safeThrowsStrawman.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ import language.experimental.erasedDefinitions
33
object scalax:
44
erased class CanThrow[-E <: Exception]
55

6-
infix type throws[R, +E <: Exception] = CanThrow[E] ?=> R
6+
infix type raises[R, +E <: Exception] = CanThrow[E] ?=> R
77

88
class Fail extends Exception
99

10-
def raise[E <: Exception](e: E): Nothing throws E = throw e
10+
def raise[E <: Exception](e: E): Nothing raises E = throw e
1111

1212
import scalax._
1313

14-
def foo(x: Boolean): Int throws Fail =
14+
def foo(x: Boolean): Int raises Fail =
1515
if x then 1 else raise(Fail())
1616

1717
def bar(x: Boolean)(using CanThrow[Fail]): Int = foo(x)
18-
def baz: Int throws Exception = foo(false)
18+
def baz: Int raises Exception = foo(false)
1919

2020
@main def Test =
2121
try

tests/run/safeThrowsStrawman2.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ import language.experimental.erasedDefinitions
33
object scalax:
44
erased class CanThrow[-E <: Exception]
55

6-
infix type throws[R, +E <: Exception] = CanThrow[E] ?=> R
6+
infix type raises[R, +E <: Exception] = CanThrow[E] ?=> R
77

88
class Fail extends Exception
99

10-
def raise[E <: Exception](e: E): Nothing throws E = throw e
10+
def raise[E <: Exception](e: E): Nothing raises E = throw e
1111

1212
private class Result[T]:
1313
var value: T = scala.compiletime.uninitialized
1414

15-
def try1[R, E <: Exception](body: => R throws E)(c: E => Unit): R =
15+
def try1[R, E <: Exception](body: => R raises E)(c: E => Unit): R =
1616
try2(body)(c) {}
1717

18-
def try2[R, E <: Exception](body: => R throws E)(c: E => Unit)(f: => Unit): R =
18+
def try2[R, E <: Exception](body: => R raises E)(c: E => Unit)(f: => Unit): R =
1919
val res = new Result[R]
2020
try
2121
given CanThrow[E] = ???
@@ -30,11 +30,11 @@ object scalax:
3030

3131
import scalax._
3232

33-
def foo(x: Boolean): Int throws Fail =
33+
def foo(x: Boolean): Int raises Fail =
3434
if x then 1 else raise(Fail())
3535

3636
def bar(x: Boolean)(using CanThrow[Fail]): Int = foo(x)
37-
def baz: Int throws Exception = foo(false)
37+
def baz: Int raises Exception = foo(false)
3838

3939
@main def Test =
4040
try1 {

tests/run/saferExceptions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def foo(x: Int) =
1717
case ex: Exception => 4
1818
case ex: Throwable => 5
1919

20-
def bar(x: Int): Int canThrow Exception =
20+
def bar(x: Int): Int throws Exception =
2121
x match
2222
case 1 => throw AssertionError()
2323
case 2 => throw Fail()

0 commit comments

Comments
 (0)