[Git][ghc/ghc][wip/exception-propagate] Exception rethrowing

Rodrigo Mesquita (@alt-romes) gitlab at gitlab.haskell.org
Tue Sep 24 15:20:49 UTC 2024



Rodrigo Mesquita pushed to branch wip/exception-propagate at Glasgow Haskell Compiler / GHC


Commits:
41d8b803 by Matthew Pickering at 2024-09-24T16:20:33+01:00
Exception rethrowing

Basic changes:

* Change `catch` function to propagate exceptions using the
  WhileHandling mechanism.
* Introduce `catchNoPropagate`, which does the same as before, but
  passes an exception which can be rethrown.
* Introduce `rethrowIO` combinator, which rethrows an exception with a
  context and doesn't add a new backtrace.
* Introduce `tryWithContext` for a variant of `try` which can rethrow
  the exception with it's original context.
* onException is modified to rethrow the original error rather than
  creating a new callstack.
* Functions which rethrow in GHC.Internal.IO.Handle.FD,
  GHC.Internal.IO.Handle.Internals, GHC.Internal.IO.Handle.Text, and
  GHC.Internal.System.IO.Error are modified to not add a new callstack.

- - - - -


10 changed files:

- libraries/base/src/Control/Exception.hs
- libraries/ghc-internal/src/GHC/Internal/Control/Exception.hs
- libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs
- libraries/ghc-internal/src/GHC/Internal/Exception/Type.hs
- libraries/ghc-internal/src/GHC/Internal/IO.hs
- libraries/ghc-internal/src/GHC/Internal/IO/Exception.hs
- libraries/ghc-internal/src/GHC/Internal/IO/Handle/FD.hs
- libraries/ghc-internal/src/GHC/Internal/IO/Handle/Internals.hs
- libraries/ghc-internal/src/GHC/Internal/IO/Handle/Text.hs
- libraries/ghc-internal/src/GHC/Internal/System/IO/Error.hs


Changes:

=====================================
libraries/base/src/Control/Exception.hs
=====================================
@@ -38,6 +38,8 @@ module Control.Exception
      someExceptionContext,
      annotateIO,
      ExceptionWithContext(..),
+     WhileHandling(..),
+
      -- * Concrete exception types
      IOException,
      ArithException(..),
@@ -65,6 +67,7 @@ module Control.Exception
      -- *  Throwing exceptions
      throw,
      throwIO,
+     rethrowIO,
      ioError,
      throwTo,
      -- *  Catching Exceptions
@@ -73,6 +76,7 @@ module Control.Exception
      -- $catchall
      -- **  The @catch@ functions
      catch,
+     catchNoPropagate,
      catches,
      Handler(..),
      catchJust,
@@ -81,6 +85,7 @@ module Control.Exception
      handleJust,
      -- **  The @try@ functions
      try,
+     tryWithContext,
      tryJust,
      -- **  The @evaluate@ function
      evaluate,
@@ -111,11 +116,11 @@ module Control.Exception
      bracketOnError,
      finally,
      onException
+
      ) where
 
 import GHC.Internal.Control.Exception
 import GHC.Internal.Exception.Type
-import GHC.Internal.IO (annotateIO)
 
 {- $catching
 


=====================================
libraries/ghc-internal/src/GHC/Internal/Control/Exception.hs
=====================================
@@ -62,20 +62,25 @@ module GHC.Internal.Control.Exception (
         -- * Throwing exceptions
         throw,
         throwIO,
+        rethrowIO,
         ioError,
         throwTo,
 
         -- ** The @catch@ functions
         catch,
+        catchNoPropagate,
         catches, Handler(..),
         catchJust,
 
+        -- ** Exception annotation
+
         -- ** The @handle@ functions
         handle,
         handleJust,
 
         -- ** The @try@ functions
         try,
+        tryWithContext,
         tryJust,
 
         -- ** The @evaluate@ function
@@ -105,6 +110,12 @@ module GHC.Internal.Control.Exception (
         finally,
         onException,
 
+        -- * Annotating exceptions
+
+        ExceptionContext(..),
+        annotateIO,
+        WhileHandling(..),
+
   ) where
 
 import GHC.Internal.Control.Exception.Base


=====================================
libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs
=====================================
@@ -46,6 +46,7 @@ module GHC.Internal.Control.Exception.Base (
 
         -- * Throwing exceptions
         throwIO,
+        rethrowIO,
         throw,
         ioError,
         throwTo,
@@ -54,6 +55,7 @@ module GHC.Internal.Control.Exception.Base (
 
         -- ** The @catch@ functions
         catch,
+        catchNoPropagate,
         catchJust,
 
         -- ** The @handle@ functions
@@ -62,6 +64,7 @@ module GHC.Internal.Control.Exception.Base (
 
         -- ** The @try@ functions
         try,
+        tryWithContext,
         tryJust,
         onException,
 
@@ -85,6 +88,13 @@ module GHC.Internal.Control.Exception.Base (
 
         assert,
 
+        -- * Annotating exceptions
+
+        ExceptionContext(..),
+        annotateIO,
+        WhileHandling(..),
+
+
         -- * Utilities
 
         bracket,
@@ -105,6 +115,7 @@ import           GHC.Internal.Base
 import           GHC.Internal.Exception
 import           GHC.Internal.IO           hiding (bracket, finally, onException)
 import           GHC.Internal.IO.Exception
+import           GHC.Internal.Exception.Type
 import           GHC.Internal.Show
 -- import GHC.Internal.Exception hiding ( Exception )
 import           GHC.Internal.Conc.Sync
@@ -132,10 +143,11 @@ catchJust
         -> IO a                   -- ^ Computation to run
         -> (b -> IO a)            -- ^ Handler
         -> IO a
-catchJust p a handler = catch a handler'
-  where handler' e = case p e of
-                        Nothing -> throwIO e
-                        Just b  -> handler b
+catchJust p a handler = catchNoPropagate a handler'
+  where handler' ec@(ExceptionWithContext _ e) =
+                    case p e of
+                        Nothing -> rethrowIO ec
+                        Just b  -> annotateIO (whileHandling ec) (handler b)
 
 -- | A version of 'catch' with the arguments swapped around; useful in
 -- situations where the code for the handler is shorter.  For example:
@@ -176,23 +188,23 @@ mapException f v = unsafePerformIO (catch (evaluate v)
 try :: Exception e => IO a -> IO (Either e a)
 try a = catch (a >>= \ v -> return (Right v)) (\e -> return (Left e))
 
+-- | Like 'try' but also returns the exception context, which is useful if you intend
+-- to rethrow the exception later.
+tryWithContext :: Exception e => IO a -> IO (Either (ExceptionWithContext e) a)
+tryWithContext a = catchNoPropagate (a >>= \v -> return (Right v)) (\e -> return (Left e))
+
 -- | A variant of 'try' that takes an exception predicate to select
 -- which exceptions are caught (c.f. 'catchJust').  If the exception
 -- does not match the predicate, it is re-thrown.
 tryJust :: Exception e => (e -> Maybe b) -> IO a -> IO (Either b a)
-tryJust p a = do
-  r <- try a
-  case r of
-        Right v -> return (Right v)
-        Left  e -> case p e of
-                        Nothing -> throwIO e
-                        Just b  -> return (Left b)
+tryJust p a = catchJust p (Right `fmap` a) (return . Left)
 
 -- | Like 'finally', but only performs the final action if there was an
 -- exception raised by the computation.
 onException :: IO a -> IO b -> IO a
-onException io what = io `catch` \e -> do _ <- what
-                                          throwIO (e :: SomeException)
+onException io what = io `catchNoPropagate` \e -> do
+                        _ <- what
+                        rethrowIO (e :: ExceptionWithContext SomeException)
 
 -----------------------------------------------------------------------------
 -- Some Useful Functions


=====================================
libraries/ghc-internal/src/GHC/Internal/Exception/Type.hs
=====================================
@@ -39,13 +39,16 @@ module GHC.Internal.Exception.Type
        , emptyExceptionContext
        , mergeExceptionContext
        , ExceptionWithContext(..)
+         -- * Exception propagation
+       , WhileHandling(..)
+       , whileHandling
          -- * Arithmetic exceptions
        , ArithException(..)
        , divZeroException, overflowException, ratioZeroDenomException
        , underflowException
        ) where
 
-import GHC.Internal.Data.OldList (intersperse)
+import GHC.Internal.Data.OldList (intersperse, lines, unlines, null)
 import GHC.Internal.Data.Maybe
 import GHC.Internal.Data.Typeable (Typeable, TypeRep, cast)
 import qualified GHC.Internal.Data.Typeable as Typeable
@@ -69,6 +72,27 @@ will be subject to defaulting, as described above.
 -}
 type HasExceptionContext = (?exceptionContext :: ExceptionContext)
 
+{- | @WhileHandling@ is used to annotate rethrow exceptions. By inspecting
+ the @WhileHandling@ annotation, all the places the exception has been rethrow
+ can be recovered.
+-}
+
+data WhileHandling = WhileHandling SomeException deriving Show
+
+instance ExceptionAnnotation WhileHandling where
+  displayExceptionAnnotation (WhileHandling e) =
+    "While handling " ++ case lines $ displayException e of
+      [] -> ""
+      (l1:ls) ->
+        -- Indent lines forward.
+        -- displayException may be ill prepared for this?...
+        unlines $ l1:[if null l then "  |" else "  | " ++ l | l <- ls]
+
+
+-- | Create 'SomeException' from an explicit context and exception.
+whileHandling :: Exception e => ExceptionWithContext e -> WhileHandling
+whileHandling e = WhileHandling (toException e)
+
 {- |
 The @SomeException@ type is the root of the exception type hierarchy.
 When an exception of type @e@ is thrown, behind the scenes it is


=====================================
libraries/ghc-internal/src/GHC/Internal/IO.hs
=====================================
@@ -40,7 +40,8 @@ module GHC.Internal.IO (
 
         FilePath,
 
-        catch, catchException, catchAny, throwIO,
+        catch, catchNoPropagate, catchException, catchExceptionNoPropagate, catchAny, throwIO,
+        rethrowIO,
         mask, mask_, uninterruptibleMask, uninterruptibleMask_,
         MaskingState(..), getMaskingState,
         unsafeUnmask, interruptible,
@@ -51,7 +52,7 @@ module GHC.Internal.IO (
 import GHC.Internal.Base
 import GHC.Internal.ST
 import GHC.Internal.Exception
-import GHC.Internal.Exception.Type (NoBacktrace(..))
+import GHC.Internal.Exception.Type (NoBacktrace(..), WhileHandling(..), HasExceptionContext, ExceptionWithContext(..))
 import GHC.Internal.Show
 import GHC.Internal.IO.Unsafe
 import GHC.Internal.Unsafe.Coerce ( unsafeCoerce )
@@ -152,6 +153,10 @@ have to work around that in the definition of catch below).
 catchException :: Exception e => IO a -> (e -> IO a) -> IO a
 catchException !io handler = catch io handler
 
+-- | A variant of 'catchException' which does not annotate the handler with 'WhileHandling'
+catchExceptionNoPropagate :: Exception e => IO a -> (ExceptionWithContext e -> IO a) -> IO a
+catchExceptionNoPropagate !io handler = catchNoPropagate io handler
+
 -- | This is the simplest of the exception-catching functions.  It
 -- takes a single argument, runs it, and if an exception is raised
 -- the \"handler\" is executed, with the value of the exception passed as an
@@ -191,6 +196,23 @@ catch   :: Exception e
         -> IO a
 -- See #exceptions_and_strictness#.
 catch (IO io) handler = IO $ catch# io handler'
+  where
+    handler' e =
+      case fromException e of
+        Just e' -> unIO (annotateIO (WhileHandling e) (handler e'))
+        Nothing -> raiseIO# e
+
+-- | A variant of 'catch' which doesn't annotate the handler with the exception
+-- which was caught. This function should be used when you are implementing your own
+-- error handling functions which may rethrow the exceptions.
+--
+-- In the case where you rethrow an exception without modifying it, you should
+-- rethrow the exception with the old exception context.
+catchNoPropagate :: Exception e
+                 => IO a
+                 -> (ExceptionWithContext e -> IO a)
+                 -> IO a
+catchNoPropagate (IO io) handler = IO $ catch# io handler'
   where
     handler' e =
       case fromException e of
@@ -202,10 +224,12 @@ catch (IO io) handler = IO $ catch# io handler'
 -- Note that this function is /strict/ in the action. That is,
 -- @catchAny undefined b == _|_ at . See #exceptions_and_strictness# for
 -- details.
-catchAny :: IO a -> (forall e . Exception e => e -> IO a) -> IO a
+--
+-- If you rethrow an exception, you should reuse the supplied ExceptionContext.
+catchAny :: IO a -> (forall e . (HasExceptionContext, Exception e) => e -> IO a) -> IO a
 catchAny !(IO io) handler = IO $ catch# io handler'
   where
-    handler' (SomeException e) = unIO (handler e)
+    handler' se@(SomeException e) = unIO (annotateIO (WhileHandling se) (handler e))
 
 -- | Execute an 'IO' action, adding the given 'ExceptionContext'
 -- to any thrown synchronous exceptions.
@@ -260,6 +284,11 @@ throwIO e = do
     se <- toExceptionWithBacktrace e
     IO (raiseIO# se)
 
+-- | A utility to use when rethrowing exceptions, no new backtrace will be attached
+-- when rethrowing an exception but you must supply the existing context.
+rethrowIO :: Exception e => ExceptionWithContext e -> IO a
+rethrowIO e = throwIO (NoBacktrace e)
+
 -- -----------------------------------------------------------------------------
 -- Controlling asynchronous exception delivery
 
@@ -332,9 +361,9 @@ getMaskingState  = IO $ \s ->
                              _  -> MaskedInterruptible #)
 
 onException :: IO a -> IO b -> IO a
-onException io what = io `catchException` \e -> do
+onException io what = io `catchExceptionNoPropagate` \e -> do
     _ <- what
-    throwIO $ NoBacktrace (e :: SomeException)
+    rethrowIO (e :: ExceptionWithContext SomeException)
 
 -- | Executes an IO computation with asynchronous
 -- exceptions /masked/.  That is, any thread which attempts to raise


=====================================
libraries/ghc-internal/src/GHC/Internal/IO/Exception.hs
=====================================
@@ -315,7 +315,7 @@ ioException     :: HasCallStack => IOException -> IO a
 ioException err = throwIO err
 
 -- | Raise an 'IOError' in the 'IO' monad.
-ioError         :: IOError -> IO a
+ioError         :: HasCallStack => IOError -> IO a
 ioError         =  ioException
 
 -- ---------------------------------------------------------------------------


=====================================
libraries/ghc-internal/src/GHC/Internal/IO/Handle/FD.hs
=====================================
@@ -1,4 +1,5 @@
 {-# LANGUAGE Trustworthy #-}
+{-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE CPP, NoImplicitPrelude #-}
 
 -----------------------------------------------------------------------------
@@ -27,12 +28,13 @@ module GHC.Internal.IO.Handle.FD (
 
 import GHC.Internal.Base
 import GHC.Internal.Show
-import GHC.Internal.Control.Exception (try)
+import GHC.Internal.Control.Exception (tryWithContext)
 import GHC.Internal.Data.Maybe
 import GHC.Internal.Data.Either (either)
 import GHC.Internal.Data.Typeable
 import GHC.Internal.Foreign.C.Types
 import GHC.Internal.MVar
+import GHC.Internal.Exception.Type
 import GHC.Internal.IO
 import GHC.Internal.IO.Encoding
 import GHC.Internal.IO.Device as IODevice
@@ -116,6 +118,18 @@ addFilePathToIOError :: String -> FilePath -> IOException -> IOException
 addFilePathToIOError fun fp ioe
   = ioe{ ioe_location = fun, ioe_filename = Just fp }
 
+-- It could be good in future to use native exception annotation here rather than
+-- modifying IOException
+catchAndAnnotate :: FilePath -> String -> IO a -> IO a
+catchAndAnnotate fp s a =
+  catchExceptionNoPropagate @IOError a
+    (\(ExceptionWithContext c e) -> rethrowIO (ExceptionWithContext c (addFilePathToIOError s fp e)))
+
+-- | Specialised 'rethrowIO' to 'IOError', helpful for type inference
+rethrowError :: ExceptionWithContext IOError -> IO a
+rethrowError = rethrowIO
+
+
 -- | Computation 'openFile' @file mode@ allocates and returns a new, open
 -- handle to manage the file @file at .  It manages input if @mode@
 -- is 'ReadMode', output if @mode@ is 'WriteMode' or 'AppendMode',
@@ -151,9 +165,9 @@ addFilePathToIOError fun fp ioe
 -- be using 'openBinaryFile'.
 openFile :: FilePath -> IOMode -> IO Handle
 openFile fp im =
-  catchException
+  catchAndAnnotate
+    fp "openFile"
     (openFile' fp im dEFAULT_OPEN_IN_BINARY_MODE True)
-    (\e -> ioError (addFilePathToIOError "openFile" fp e))
 
 -- | @'withFile' name mode act@ opens a file like 'openFile' and passes
 -- the resulting handle to the computation @act at .  The handle will be
@@ -166,10 +180,9 @@ openFile fp im =
 withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
 withFile fp im act = do
   -- Only annotate when setup or teardown of withFile' raised the exception
-  catchException
-    (withFile' fp im dEFAULT_OPEN_IN_BINARY_MODE True (try . act))
-    (\e -> ioError (addFilePathToIOError "withFile" fp e))
-    >>= either ioError pure
+  catchAndAnnotate fp "withFile"
+    (withFile' fp im dEFAULT_OPEN_IN_BINARY_MODE True (tryWithContext . act))
+    >>= either rethrowError pure
 
 -- | Like 'openFile', but opens the file in ordinary blocking mode.
 -- This can be useful for opening a FIFO for writing: if we open in
@@ -187,9 +200,8 @@ withFile fp im act = do
 -- @since base-4.4.0.0
 openFileBlocking :: FilePath -> IOMode -> IO Handle
 openFileBlocking fp im =
-  catchException
+  catchAndAnnotate fp "openFileBlocking"
     (openFile' fp im dEFAULT_OPEN_IN_BINARY_MODE False)
-    (\e -> ioError (addFilePathToIOError "openFileBlocking" fp e))
 
 -- | @'withFileBlocking' name mode act@ opens a file like 'openFileBlocking'
 -- and passes the resulting handle to the computation @act at .  The handle will
@@ -202,10 +214,9 @@ openFileBlocking fp im =
 withFileBlocking :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
 withFileBlocking fp im act = do
   -- Only annotate when setup or teardown of withFile' raised the exception
-  catchException
-    (withFile' fp im dEFAULT_OPEN_IN_BINARY_MODE False (try . act))
-    (\e -> ioError (addFilePathToIOError "withFileBlocking" fp e))
-    >>= either ioError pure
+  catchAndAnnotate fp "withFileBlocking"
+    (withFile' fp im dEFAULT_OPEN_IN_BINARY_MODE False (tryWithContext . act))
+    >>= either rethrowError pure
 
 -- | Like 'openFile', but open the file in binary mode.
 -- On Windows, reading a file in text mode (which is the default)
@@ -220,9 +231,8 @@ withFileBlocking fp im act = do
 -- described in "Control.Exception".
 openBinaryFile :: FilePath -> IOMode -> IO Handle
 openBinaryFile fp m =
-  catchException
+  catchAndAnnotate fp "openBinaryFile"
     (openFile' fp m True True)
-    (\e -> ioError (addFilePathToIOError "openBinaryFile" fp e))
 
 -- | A version of `openBinaryFile` that takes an action to perform
 -- with the handle. If an exception occurs in the action, then
@@ -234,10 +244,9 @@ openBinaryFile fp m =
 withBinaryFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
 withBinaryFile fp im act =
   -- Only annotate when setup or teardown of withFile' raised the exception
-  catchException
-    (withFile' fp im True True (try . act))
-    (\e -> ioError (addFilePathToIOError "withBinaryFile" fp e))
-    >>= either ioError pure
+  catchAndAnnotate fp "withBinaryFile"
+    (withFile' fp im True True (tryWithContext . act))
+    >>= either rethrowError pure
 
 -- | Open a file and perform an action with it. If the action throws an
 -- exception, then the file will be closed. If the last argument is 'True',


=====================================
libraries/ghc-internal/src/GHC/Internal/IO/Handle/Internals.hs
=====================================
@@ -79,6 +79,7 @@ import GHC.Internal.Real
 import GHC.Internal.Word
 import GHC.Internal.Base
 import GHC.Internal.Exception
+import GHC.Internal.Exception.Type
 import GHC.Internal.Num          ( Num(..) )
 import GHC.Internal.Show
 import GHC.Internal.IORef
@@ -178,13 +179,13 @@ do_operation :: String -> Handle -> (Handle__ -> IO a) -> MVar Handle__ -> IO a
 do_operation fun h act m = do
   h_ <- takeMVar m
   checkHandleInvariants h_
-  act h_ `catchException` handler h_
+  act h_ `catchExceptionNoPropagate` handler h_
   where
-    handler h_ e = do
+    handler h_ (ExceptionWithContext c e) = do
       putMVar m h_
       case () of
         _ | Just ioe <- fromException e ->
-            ioError (augmentIOError ioe fun h)
+            rethrowIO (ExceptionWithContext c $ augmentIOError ioe fun h)
         _ | Just async_ex <- fromException e -> do -- see Note [async]
             let _ = async_ex :: SomeAsyncException
             t <- myThreadId


=====================================
libraries/ghc-internal/src/GHC/Internal/IO/Handle/Text.hs
=====================================
@@ -41,6 +41,7 @@ import GHC.Internal.IO.Buffer
 import qualified GHC.Internal.IO.BufferedIO as Buffered
 import GHC.Internal.IO.Exception
 import GHC.Internal.Exception
+import GHC.Internal.Exception.Type
 import GHC.Internal.IO.Handle.Types
 import GHC.Internal.IO.Handle.Internals
 import qualified GHC.Internal.IO.Device as IODevice
@@ -495,7 +496,7 @@ hGetContents' handle = do
       Left e ->
           case fromException e of
             Just ioe -> throwIO (augmentIOError ioe "hGetContents'" handle)
-            Nothing -> throwIO e
+            Nothing -> throwIO (NoBacktrace e)
 
 strictRead :: Handle -> Handle__ -> IO (Handle__, Either SomeException String)
 strictRead h handle_ at Handle__{..} = do


=====================================
libraries/ghc-internal/src/GHC/Internal/System/IO/Error.hs
=====================================
@@ -90,6 +90,7 @@ import GHC.Internal.Data.Either
 import GHC.Internal.Data.Maybe
 
 import GHC.Internal.Base
+import GHC.Internal.Exception.Type
 import GHC.Internal.IO
 import GHC.Internal.IO.Exception
 import GHC.Internal.IO.Handle.Types
@@ -320,7 +321,7 @@ ioeSetFileName    ioe filename = ioe{ ioe_filename = Just filename }
 -- | Catch any 'IOError' that occurs in the computation and throw a
 -- modified version.
 modifyIOError :: (IOError -> IOError) -> IO a -> IO a
-modifyIOError f io = catch io (\e -> ioError (f e))
+modifyIOError f io = catch io (\e -> throwIO (NoBacktrace $ f e))
 
 -- -----------------------------------------------------------------------------
 -- annotating an IOError



View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/41d8b8034624aecea1d38213df165cb3f4b874d0

-- 
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/41d8b8034624aecea1d38213df165cb3f4b874d0
You're receiving this email because of your account on gitlab.haskell.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/ghc-commits/attachments/20240924/80e7e1ba/attachment-0001.html>


More information about the ghc-commits mailing list