Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling of multiple exceptions for saferExceptions #13914

Merged
merged 2 commits into from Dec 10, 2021

Conversation

prolativ
Copy link
Contributor

@prolativ prolativ commented Nov 9, 2021

  • Generate a single accumulated CanThrow capability for multiple catch cases in a try
  • Allow parentheses around exceptions alternatives in throws clauses

Fixes #13816

* Generate a single accumulated CanThrow capability for multiple catch cases in a try
* Allow parentheses around exceptions alternatives in throws clauses
@prolativ
Copy link
Contributor Author

@odersky could you have a look at the PR?

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this scenario?

def foo() throws E1 | E2 = ...

try
  try foo()
  catch case ex: E1 => ...
catch case ex: E2 => ...

That should compile. Does it?

if Feature.enabled(Feature.saferExceptions) then
for
CaseDef(pat, guard, _) <- cases
tpe = pat.tpe.widen
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that an = in a for expression is horribly expensive, so it's actually better to do pat.tpe.widen twice.

caughtExceptions match
case Nil => expr
case head :: tail =>
val capabilityProof = tail.foldLeft(head: Type)(OrType(_, _, true))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not:

if caughtExceptions.isEmpty then expr
else
  val capabilityProof = caughtExceptions.reduce(OrType(_, _, true))
  ...

@odersky odersky assigned Kordyjan and unassigned odersky Dec 3, 2021
@prolativ
Copy link
Contributor Author

prolativ commented Dec 3, 2021

Catching multiple exceptions in nested try blocks does work in general. I'll add an explicit test for that.
The only corner case is when the capability is declared with (using CanThrow[Ex1 | Ex2])
then

  catch
    case _: Ex1 =>
    case _: Ex2 =>

will work (which is already an improvement when compared to the current state) but

    catch
      case _: Ex1 =>
  catch
    case _: Ex2 =>

will fail but this doesn't work without my changes either so there's no regression. Also the docs explicitly say that (using CanThrow[E1 | E2]) syntax might cause some problems so it's better to use any of the alternative syntaxes.
We might try to improve this somehow by one of the following:

  1. Introducing a generic mechanism for merging capabilities (this would probably require quite a lot of research)
  2. Improving the desugaring of nested try blocks so that
try
  try
    foo()
  catch
    case _: Ex1 =>
  catch
    case _: Ex2 =>

gets desugared to

try
  erased given CanThrow[Ex2] = ???
  try
    erased given CanThrow[Ex1 | Ex2] = ???
    foo()
  catch
    case _: Ex1 =>
  catch
    case _: Ex2 =>

instead of

try
  erased given CanThrow[Ex2] = ???
  try
    erased given CanThrow[Ex1] = ???
    foo()
  catch
    case _: Ex1 =>
  catch
    case _: Ex2 =>

(1) would solve both the problem that (2) fixes and the corner case described in the docs when someone tries to call a method defined with a merged capability(using CanThrow[Ex1 | Ex2]) from a method with independent capabilities (using CanThrow[Ex1], CanThrow[Ex2]).

When I think about it now, (2) shouldn't be too difficult to implement but still this could be done in a separate PR.

@odersky
Copy link
Contributor

odersky commented Dec 3, 2021

The only corner case is when the capability is declared with (using CanThrow[Ex1 | Ex2])
then

what if it is declared as throws Ex1 | Ex2 (as in my example). Would that not be the same?

@prolativ prolativ assigned prolativ and unassigned Kordyjan Dec 3, 2021
@prolativ
Copy link
Contributor Author

prolativ commented Dec 3, 2021

A throws Ex1 | Ex2

desugars to

A $throws Ex1 $throws Ex2

Which would effectively work like

(using CanThrow[Ex1])(using CanThrow[Ex2]): A

in signatures of methods, not like

(using CanThrow[Ex1 | Ex2]): A

@odersky
Copy link
Contributor

odersky commented Dec 3, 2021

Ah yes, that answers it. Thanks!

So I think except for the other minor remarks, LGTM

@prolativ
Copy link
Contributor Author

prolativ commented Dec 3, 2021

Reworks done. I also slightly improved the implicitNotFound message as I was often confused with the previous one.

@prolativ prolativ assigned odersky and unassigned prolativ Dec 3, 2021
@prolativ prolativ requested a review from odersky December 6, 2021 10:04
@prolativ
Copy link
Contributor Author

@odersky can we get this merged?

@odersky
Copy link
Contributor

odersky commented Dec 10, 2021

Yes, LGTM.

@odersky odersky merged commit c83a29e into scala:master Dec 10, 2021
@odersky odersky deleted the multicatch-safer-exceptions branch December 10, 2021 11:16
@Kordyjan Kordyjan added this to the 3.1.2 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Safer exceptions with many cases in catch
3 participants