Skip to content

Commit 045c8bd

Browse files
authored
Merge pull request #13866 from dotty-staging/fix-13846
Refine handling of CanThrow capabilities
2 parents ae0f828 + 6381d9d commit 045c8bd

File tree

9 files changed

+121
-5
lines changed

9 files changed

+121
-5
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import NameKinds.DefaultGetterName
3333
import NameOps._
3434
import SymDenotations.{NoCompleter, NoDenotation}
3535
import Applications.unapplyArgs
36+
import Inferencing.isFullyDefined
3637
import transform.patmat.SpaceEngine.isIrrefutable
3738
import config.Feature
3839
import config.Feature.sourceVersion
@@ -1362,6 +1363,21 @@ trait Checking {
13621363
def checkCanThrow(tp: Type, span: Span)(using Context): Unit =
13631364
if Feature.enabled(Feature.saferExceptions) && tp.isCheckedException then
13641365
ctx.typer.implicitArgTree(defn.CanThrowClass.typeRef.appliedTo(tp), span)
1366+
1367+
/** Check that catch can generate a good CanThrow exception */
1368+
def checkCatch(pat: Tree, guard: Tree)(using Context): Unit = pat match
1369+
case Typed(_: Ident, tpt) if isFullyDefined(tpt.tpe, ForceDegree.none) && guard.isEmpty =>
1370+
// OK
1371+
case Bind(_, pat1) =>
1372+
checkCatch(pat1, guard)
1373+
case _ =>
1374+
val req =
1375+
if guard.isEmpty then "for cases of the form `ex: T` where `T` is fully defined"
1376+
else "if no pattern guard is given"
1377+
report.error(
1378+
em"""Implementation restriction: cannot generate CanThrow capability for this kind of catch.
1379+
|CanThrow capabilities can only be generated $req.""",
1380+
pat.srcPos)
13651381
}
13661382

13671383
trait ReChecking extends Checking {
@@ -1375,6 +1391,7 @@ trait ReChecking extends Checking {
13751391
override def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = ()
13761392
override def checkNoModuleClash(sym: Symbol)(using Context) = ()
13771393
override def checkCanThrow(tp: Type, span: Span)(using Context): Unit = ()
1394+
override def checkCatch(pat: Tree, guard: Tree)(using Context): Unit = ()
13781395
}
13791396

13801397
trait NoChecking extends ReChecking {

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,9 +1751,12 @@ class Typer extends Namer
17511751
.withSpan(expr.span)
17521752
val caps =
17531753
for
1754-
CaseDef(pat, _, _) <- cases
1754+
case CaseDef(pat, guard, _) <- cases
17551755
if Feature.enabled(Feature.saferExceptions) && pat.tpe.widen.isCheckedException
1756-
yield makeCanThrow(pat.tpe.widen)
1756+
yield
1757+
checkCatch(pat, guard)
1758+
makeCanThrow(pat.tpe.widen)
1759+
17571760
caps.foldLeft(expr)((e, g) => untpd.Block(g :: Nil, e))
17581761

17591762
def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = {
@@ -1942,14 +1945,20 @@ class Typer extends Namer
19421945
}
19431946
var checkedArgs = preCheckKinds(args1, paramBounds)
19441947
// check that arguments conform to bounds is done in phase PostTyper
1945-
if (tpt1.symbol == defn.andType)
1948+
val tycon = tpt1.symbol
1949+
if (tycon == defn.andType)
19461950
checkedArgs = checkedArgs.mapconserve(arg =>
19471951
checkSimpleKinded(checkNoWildcard(arg)))
1948-
else if (tpt1.symbol == defn.orType)
1952+
else if (tycon == defn.orType)
19491953
checkedArgs = checkedArgs.mapconserve(arg =>
19501954
checkSimpleKinded(checkNoWildcard(arg)))
1955+
else if tycon == defn.throwsAlias
1956+
&& checkedArgs.length == 2
1957+
&& checkedArgs(1).tpe.derivesFrom(defn.RuntimeExceptionClass)
1958+
then
1959+
report.error(em"throws clause cannot be defined for RuntimeException", checkedArgs(1).srcPos)
19511960
else if (ctx.isJava)
1952-
if (tpt1.symbol eq defn.ArrayClass) then
1961+
if tycon eq defn.ArrayClass then
19531962
checkedArgs match {
19541963
case List(arg) =>
19551964
val elemtp = arg.tpe.translateJavaArrayElementType

docs/docs/reference/experimental/canthrow.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,20 @@ closure may refer to capabilities in its free variables. This means that `map` i
179179
already effect polymorphic even though we did not change its signature at all.
180180
So the takeaway is that the effects as capabilities model naturally provides for effect polymorphism whereas this is something that other approaches struggle with.
181181

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+
182196
## Gradual Typing Via Imports
183197

184198
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/neg/i13846.check

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- Error: tests/neg/i13846.scala:3:22 ----------------------------------------------------------------------------------
2+
3 |def foo(): Int throws ArithmeticException = 1 / 0 // error
3+
| ^^^^^^^^^^^^^^^^^^^
4+
| throws clause cannot be defined for RuntimeException
5+
-- Error: tests/neg/i13846.scala:7:9 -----------------------------------------------------------------------------------
6+
7 | foo() // error
7+
| ^
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
13+
|
14+
| The following import might fix the problem:
15+
|
16+
| import unsafeExceptions.canThrowAny
17+
|

tests/neg/i13846.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import language.experimental.saferExceptions
2+
3+
def foo(): Int throws ArithmeticException = 1 / 0 // error
4+
5+
def test(): Unit =
6+
try
7+
foo() // error
8+
catch
9+
case _: ArithmeticException => println("Caught")

tests/neg/i13849.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Error: tests/neg/i13849.scala:16:11 ---------------------------------------------------------------------------------
2+
16 | case _: Ex if false => println("Caught") // error
3+
| ^^^^^
4+
| Implementation restriction: cannot generate CanThrow capability for this kind of catch.
5+
| CanThrow capabilities can only be generated if no pattern guard is given.

tests/neg/i13849.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import annotation.experimental
2+
import language.experimental.saferExceptions
3+
4+
@experimental
5+
case class Ex(i: Int) extends Exception(s"Exception: $i")
6+
7+
@experimental
8+
def foo(): Unit throws Ex = throw Ex(1)
9+
10+
@experimental
11+
object Main:
12+
def main(args: Array[String]): Unit =
13+
try
14+
foo()
15+
catch
16+
case _: Ex if false => println("Caught") // error

tests/neg/i13864.check

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Error: tests/neg/i13864.scala:11:9 ----------------------------------------------------------------------------------
2+
11 | case Ex(i: Int) => println("Caught an Int") // error
3+
| ^^^^^^^^^^
4+
| Implementation restriction: cannot generate CanThrow capability for this kind of catch.
5+
| CanThrow capabilities can only be generated for cases of the form `ex: T` where `T` is fully defined.
6+
-- Error: tests/neg/i13864.scala:9:10 ----------------------------------------------------------------------------------
7+
9 | foo(1) // error
8+
| ^
9+
| The capability to throw exception Ex[Int] is missing.
10+
| 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]
14+
|
15+
| The following import might fix the problem:
16+
|
17+
| import unsafeExceptions.canThrowAny
18+
|

tests/neg/i13864.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import language.experimental.saferExceptions
2+
3+
case class Ex[A](a: A) extends Exception(s"Ex: $a")
4+
5+
def foo[A](a: A): Unit throws Ex[A] = throw new Ex(a)
6+
7+
def test(): Unit =
8+
try
9+
foo(1) // error
10+
catch
11+
case Ex(i: Int) => println("Caught an Int") // error

0 commit comments

Comments
 (0)