libclang-bindings-0.1.0.0: src/Clang/HighLevel/Fold.hs
-- | Higher-level bindings for traversing the API
--
-- Intended for unqualified import.
module Clang.HighLevel.Fold (
-- * Folds
Fold -- opaque
, HandlerResult(..)
, Next -- opaque
-- * Construction
, simpleFold
, foldWithHandler
, FoldException (..)
, foldTry
-- * Fold-specific operations
, foldBreak
, foldBreakWith
, foldBreakOpt
, foldContinue
, foldContinueWith
, foldContinueOpt
, foldRecurse
, foldRecurseWith
, foldRecurseOpt
, foldRecursePure
, foldRecursePureOpt
-- * Execution
, clang_visitChildren
) where
import Control.Exception (Exception (..))
import Control.Exception qualified as Base
import Control.Monad (forM_)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.IO.Unlift (MonadUnliftIO (withRunInIO))
import Data.IORef
import GHC.Stack
import Clang.Enum.Simple
import Clang.Internal.Exception
import Clang.LowLevel.Core hiding (clang_visitChildren)
import Clang.LowLevel.Core qualified as Core
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
-- | Typed fold over the AST
--
-- This is similar to @CXCursorVisitor@, but
--
-- * we allow for (typed) results
-- * when recursing into the children of a node, we get to specify a /different/
-- function
--
-- This provides for a much nicer user experience.
data Fold m a = Fold{
foldNext :: CXCursor -> m (Next m a)
, foldHandler :: CXCursor -> ExactException -> m (HandlerResult (Maybe a))
}
-- | Construct simple fold
--
-- See also 'foldWithHandler'.
simpleFold :: forall m a. MonadIO m => (CXCursor -> m (Next m a)) -> Fold m a
simpleFold foldNext =
Fold{foldNext, foldHandler}
where
foldHandler :: CXCursor -> ExactException -> m (HandlerResult (Maybe a))
foldHandler _curr _e = return HandlerRethrow
-- | Fold with exception handler
--
-- The exception handler is provided a 'CXCursor' pointing to the location in
-- the AST where the exception was caught.
--
-- == Exception handling in folds
--
-- Suppose we have a C file that looks like this:
--
-- > struct foo {
-- > int a;
-- > int b;
-- > };
-- >
-- > struct bar {
-- > int c;
-- > int d;
-- > };
-- >
-- > struct baz {
-- > int e;
-- > int f;
-- > };
--
-- and suppose we want to define a fold that extracts all struct field names,
-- so that we end up with
--
-- > [["a", "b"], ["c", "d"], ["e", "f"]]
--
-- We can write such a fold as follows:
--
-- > foldStruct :: Fold IO [Text]
-- > foldStruct = simpleFold $ \curr -> do
-- > kind <- fromSimpleEnum <$> clang_getCursorKind curr
-- > case kind of
-- > Right CXCursor_StructDecl ->
-- > foldRecursePure foldField concat
-- > _otherwise ->
-- > error $ "unexpected: " ++ show kind
-- >
-- > foldField :: Fold IO [Text]
-- > foldField = simpleFold $ \curr -> do
-- > kind <- fromSimpleEnum <$> clang_getCursorKind curr
-- > case kind of
-- > Right CXCursor_FieldDecl -> do
-- > name <- clang_getCursorSpelling curr
-- > foldContinueWith [name]
-- > _otherwise ->
-- > error $ "unexpected: " ++ show kind
--
-- Let's consider what happens if 'foldField' throws an exception; for example,
-- perhaps it throws an exception when it sees the name \"c\":
--
-- > data UnexpectedField = UnexpectedField
-- >
-- > foldField :: Fold IO [Text]
-- > foldField = simpleFold $ \curr -> do
-- > kind <- fromSimpleEnum <$> clang_getCursorKind curr
-- > case kind of
-- > Right CXCursor_FieldDecl -> do
-- > name <- clang_getCursorSpelling curr
-- > if name == "c"
-- > then throwIO UnexpectedField
-- > else foldContinueWith [name]
-- > _otherwise ->
-- > error $ "unexpected: " ++ show kind
--
-- Perhaps we want to try and recover from this in 'foldStruct', returning the
-- empty list of fields for that particular struct:
--
-- > hasUnexpectedField :: UnexpectedField -> [Text]
-- > hasUnexpectedField UnexpectedField = []
-- >
-- > foldStruct :: Fold IO [Text]
-- > foldStruct = simpleFold $ \curr ->
-- > handle (foldContinueWith . hasUnexpectedField) $ do
-- > -- .. body as before
--
-- Unfortunately, this will not work (that is, the handler will not catch the
-- exception). The problem is that 'foldStruct' does not /really/ recurse to
-- parse the fields: instead, it /returns/ a value which indicates that we are
-- interested in the children of the node, along with a function that processes
-- the results of parsing those child nodes (see 'foldRecurseWith').
--
-- Instead of using a local exception handler, you must therefore use
-- 'foldWithHandler':
--
-- > foldStruct :: Fold IO [Text]
-- > foldStruct =
-- > foldWithHandler (\_curr -> return . HandlerResult . Just . hasUnexpectedField) $ \curr -> do
-- > -- .. body as before
--
-- With this handler in place, we will get the expected result
--
-- > [["a", "b"], [], ["e", "f"]]
--
-- Put another way, 'foldWithHandler' can be used to install an exception handler
-- which behaves /as if/ the fold truly made recursive calls.
foldWithHandler :: forall m e a.
(MonadIO m, Exception e)
=> (CXCursor -> e -> m (HandlerResult (Maybe a))) -- ^ Exception handler
-> (CXCursor -> m (Next m a))
-> Fold m a
foldWithHandler handler foldNext =
Fold{foldNext, foldHandler}
where
foldHandler :: CXCursor -> ExactException -> m (HandlerResult (Maybe a))
foldHandler curr (WrapExactException se) =
case fromException se of
Just e' -> handler curr e'
Nothing -> return HandlerRethrow
-- | An exception that is caught during folding
data FoldException e = FoldException {
-- | The exception proper
exception :: e
-- | A cursor pointing to the location in the AST where the exception was
-- caught
, cursor :: CXCursor
}
deriving stock Show
-- | Like 'foldWithHandler', but return the caught exception as a value like
-- 'Control.Exception.try' would.
foldTry ::
forall e m a. (MonadIO m, Exception e)
=> (CXCursor -> m (Next m a))
-> Fold m (Either (FoldException e) a)
foldTry foldNext = foldWithHandler handler foldNext'
where
foldNext' ::
CXCursor
-> m (Next m (Either (FoldException e) a))
foldNext' curr = fmap (fmap Right) (foldNext curr)
handler ::
CXCursor
-> ExactException
-> m (HandlerResult (Maybe (Either (FoldException e) a)))
handler curr (WrapExactException se)
| Just e <- fromException @e se = do
return $ HandlerResult $ Just $ Left $ FoldException {
exception = e
, cursor = curr
}
| otherwise = return $ HandlerRethrow
-- | Result of visiting one node
--
-- This is the equivalent of 'CXChildVisitResult'
data Next m a where
Break :: Maybe a -> Next m a
Continue :: Maybe a -> Next m a
Recurse :: Fold m b -> ([b] -> m (Maybe a)) -> Next m a
{-------------------------------------------------------------------------------
Constructing 'Next' ('Next' itself is intentionally opaque)
-------------------------------------------------------------------------------}
-- | Stop folding early, without a result
--
-- See also 'foldBreakWith' and 'foldBreakOpt'.
--
-- NOTE: \"break\" means that the fold is terminated entirely; it does /not/
-- mean \"break to the immediate parent\".
--
-- This is the equivalent of 'CXChildVisit_Break'.
foldBreak :: Monad m => m (Next m a)
foldBreak = foldBreakOpt Nothing
-- | Like 'foldBreak', but producing a result
foldBreakWith :: Monad m => a -> m (Next m a)
foldBreakWith = foldBreakOpt . Just
-- | Generalization of 'foldBreak' and 'foldBreakWith' with optional result
foldBreakOpt :: Monad m => Maybe a -> m (Next m a)
foldBreakOpt = pure . Break
-- | Continue with the next sibling of the current node, without a result
--
-- See also 'foldContinueWith' and 'foldContinueOpt'.
--
-- This is the equivalent of 'CXChildVisit_Continue'.
foldContinue :: Monad m => m (Next m a)
foldContinue = foldContinueOpt Nothing
-- | Like 'foldContinue', but producing a result
foldContinueWith :: Monad m => a -> m (Next m a)
foldContinueWith = foldContinueOpt . Just
-- | Generalization of 'foldContinue' and 'foldContinueWith' with optional result
foldContinueOpt :: Monad m => Maybe a -> m (Next m a)
foldContinueOpt = pure . Continue
-- | Recurse into the children of the current node, without producing a result
--
-- Unlike in the low-level @clang@ interface, each time we recurse we can
-- use a /different/ fold.
--
-- See also 'foldRecurseWith' if you want the results of the recursion.
--
-- This is the equivalent of 'CXChildVisit_Recurse'.
foldRecurse :: Monad m => Fold m () -> m (Next m a)
foldRecurse fold = foldRecursePureOpt fold (const Nothing)
-- | Like 'foldRecurse', but producing a result
--
-- In this case, we must provide a @summarize@ function which turns the results
-- obtained from processing the children into a result for the parent.
foldRecurseWith :: Monad m => Fold m b -> ([b] -> m a) -> m (Next m a)
foldRecurseWith fold summarize = foldRecurseOpt fold (fmap Just . summarize)
-- | Generalization of 'foldRecurseWith' with an /optional/ result
foldRecurseOpt :: Monad m => Fold m b -> ([b] -> m (Maybe a)) -> m (Next m a)
foldRecurseOpt fold summarize = pure $ Recurse fold summarize
-- | Pure variant on 'foldRecurseWith'
foldRecursePure :: Monad m => Fold m b -> ([b] -> a) -> m (Next m a)
foldRecursePure fold summarize = foldRecurseWith fold (pure . summarize)
-- | Pure variant on 'foldRecurseOpt'
foldRecursePureOpt :: Monad m => Fold m b -> ([b] -> Maybe a) -> m (Next m a)
foldRecursePureOpt fold summarize = foldRecurseOpt fold (pure . summarize)
{-------------------------------------------------------------------------------
Combinators
-------------------------------------------------------------------------------}
instance Functor m => Functor (Next m) where
fmap f (Break x) = Break (fmap f x)
fmap f (Continue x) = Continue (fmap f x)
fmap f (Recurse r g) = Recurse r (fmap (fmap f) . g)
instance Functor m => Functor (Fold m) where
fmap f Fold{foldNext, foldHandler} = Fold{
foldNext = \curr -> fmap (fmap f) $ foldNext curr
, foldHandler = \curr -> fmap (fmap (fmap f)) . foldHandler curr
}
{-------------------------------------------------------------------------------
Internal: partial results
NOTE: These functions are ultimately called from a Haskel callback called from
the clang C library. They do not need to be (nor are) thread safe.
-------------------------------------------------------------------------------}
type PartialResults a = IORef (Either ExactException [a])
newPartialResults :: IO (PartialResults a)
newPartialResults = newIORef (Right [])
addPartialResult :: HasCallStack => PartialResults a -> a -> IO ()
addPartialResult ref x = do
mResults <- readIORef ref
case mResults of
Left oldErr -> unexpectedException oldErr
Right xs -> writeIORef ref $ Right (x:xs)
recordException :: HasCallStack => PartialResults a -> ExactException -> IO ()
recordException ref newErr = do
mResults <- readIORef ref
case mResults of
Left oldErr -> unexpectedException oldErr
Right _xs -> writeIORef ref $ Left newErr
-- | Existing exceptions are impossible
--
-- As soon as any partial result throws an exception, we skip over any of the
-- remaining children, and immediately to the @summarize@ function of the
-- parent. This means that an existing exception should be impossible.
--
-- The use of @error@ here is an exception in its own right, which we don't
-- propagate (it will be caught in the low-level 'Core.clang_visitChildren'
-- function). However, that's okay: if this @error@ ever triggers, it indicates
-- a bug in this infrastructure, which can't really be handlded anyway.
unexpectedException :: HasCallStack => ExactException -> IO a
unexpectedException oldErr = error $ concat [
"The impossible happened: we break at the first exception, "
, "yet here we are: " ++ show oldErr ++ ".\n"
, prettyCallStack callStack
]
getPartialResults :: MonadIO m => PartialResults a -> m [a]
getPartialResults ref = liftIO $ do
mResults <- readIORef ref
case mResults of
Left e -> throwExact e
Right xs -> return (reverse xs)
partialResultsIsException :: PartialResults a -> IO Bool
partialResultsIsException ref = do
mResults <- readIORef ref
case mResults of
Left _e -> return True
Right _xs -> return False
{-------------------------------------------------------------------------------
Internal: stack
-------------------------------------------------------------------------------}
data Processing m a = Processing {
-- | The AST node whose children we are processing
parent :: CXCursor
-- | The 'Fold' we are applying at this level
, currentFold :: Fold m a
-- | Results collected so far (in reverse order)
, partialResults :: PartialResults a
}
data Stack m a where
Bottom :: Processing m a -> Stack m a
Push :: Processing m a -> ([a] -> m (Maybe b)) -> Stack m b -> Stack m a
topProcessing :: Stack m a -> Processing m a
topProcessing (Bottom p) = p
topProcessing (Push p _ _) = p
topParent :: Stack m a -> CXCursor
topParent = parent . topProcessing
topFold :: Stack m a -> Fold m a
topFold = currentFold . topProcessing
topResults :: Stack m a -> PartialResults a
topResults = partialResults . topProcessing
data SomeStack m where
SomeStack :: Stack m a -> SomeStack m
initStack ::
CXCursor
-> Fold m a
-> IO (Stack m a)
initStack root topLevelFold = do
partialResults <- newPartialResults
let p = Processing {
parent = root
, currentFold = topLevelFold
, partialResults
}
return $ Bottom p
push ::
CXCursor
-> Fold m b
-> ([b] -> m (Maybe a))
-> Stack m a -> IO (Stack m b)
push newParent fold summarize stack = do
partialResults <- newPartialResults
let p = Processing {
parent = newParent
, currentFold = fold
, partialResults
}
return $ Push p summarize stack
popUntil :: forall m.
MonadIO m
=> RunInIO m
-> IORef (SomeStack m)
-> CXCursor -> IO ()
popUntil runInIO someStack newParent = do
SomeStack stack <- liftIO $ readIORef someStack
stack' <- loop stack
liftIO $ writeIORef someStack stack'
where
loop :: Stack m a -> IO (SomeStack m)
loop stack = do
arrived <- clang_equalCursors (topParent stack) newParent
if arrived then
return $ SomeStack stack
else
case stack of
Bottom _ ->
error "popUntil: something has gone horribly wrong"
Push p summarize (stack' :: Stack m b) -> do
let handler :: ExactException -> m (HandlerResult (Maybe b))
handler = foldHandler (topFold stack') (parent p)
mb <- Base.try $
handleUnliftUsing runInIO handler $
summarize =<< getPartialResults (partialResults p)
case mb of
Right Nothing -> return ()
Right (Just b) -> addPartialResult (topResults stack') b
Left ex -> recordException (topResults stack') ex
loop stack'
{-------------------------------------------------------------------------------
Traversal proper
-------------------------------------------------------------------------------}
-- | Fold the AST
--
-- This provides a higher-level API to 'Core.clang_visitChildren', in which
--
-- * visitors can return results
-- * we can specify different visitors at different levels of the AST
--
-- See also 'clang_getTranslationUnitCursor'.
clang_visitChildren :: forall m a.
MonadUnliftIO m
=> CXCursor -> Fold m a -> m [a]
clang_visitChildren root topLevelFold = withRunInIO $ \runInIO -> do
stack <- initStack root topLevelFold
someStack <- newIORef $ SomeStack stack
_terminatedEarly <- Core.clang_visitChildren root $ visitor runInIO someStack
popUntil runInIO someStack root
getPartialResults (topResults stack)
where
visitor ::
RunInIO m
-> IORef (SomeStack m)
-> CXCursor
-> CXCursor
-> IO (SimpleEnum CXChildVisitResult)
visitor runInIO someStack current parent = do
popUntil runInIO someStack parent
SomeStack (stack :: Stack m x) <- readIORef someStack
let foldHandler :: CXCursor -> ExactException -> m (HandlerResult (Maybe x))
foldNext :: CXCursor -> m (Next m x)
partialResults :: PartialResults x
Processing{
currentFold = Fold{foldHandler, foldNext}
, partialResults
} = topProcessing stack
previousException <- partialResultsIsException partialResults
if previousException then
return $ simpleEnum CXChildVisit_Continue
else do
next <- Base.try $
handleUnliftUsing runInIO (fmap (fmap Continue) . foldHandler current) $
foldNext current
case next of
Right (Break ma) -> do
forM_ ma $ addPartialResult partialResults
return $ simpleEnum CXChildVisit_Break
Right (Continue ma) -> do
forM_ ma $ addPartialResult partialResults
return $ simpleEnum CXChildVisit_Continue
Right (Recurse fold summarize) -> do
stack' <- push current fold summarize stack
writeIORef someStack $ SomeStack stack'
return $ simpleEnum CXChildVisit_Recurse
Left ex -> do
recordException partialResults ex
return $ simpleEnum CXChildVisit_Continue