hs-bindgen-1.0.0.0: clang-ast-dump/Main.hs
{-# LANGUAGE ApplicativeDo #-}
module Main (main) where
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Data.Bifunctor
import Data.Default (Default (def))
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Foreign.C.Types (CUInt)
import GHC.Generics (Generic)
import Options.Applicative qualified as OA
import System.Exit (exitFailure)
import Clang.Enum.Bitfield
import Clang.Enum.Simple
import Clang.HighLevel qualified as HighLevel
import Clang.HighLevel.Types hiding (FoldException (..))
import Clang.LowLevel.Core
import Clang.LowLevel.Doxygen
import Clang.Paths
import HsBindgen.Clang
import HsBindgen.Config.ClangArgs
import HsBindgen.IR.C qualified as C
import HsBindgen.Resolve (resolveHeaders)
import HsBindgen.TraceMsg
import HsBindgen.Util.Tracer
{-------------------------------------------------------------------------------
Options
-------------------------------------------------------------------------------}
data Options = Options {
builtin :: Bool
, comments :: Bool
, extents :: Bool
, file :: C.HashIncludeArg
, kind :: Bool
, includePath :: [FilePath]
, clangArgsInner :: [String]
, sameFile :: Bool
, typ :: Bool
}
{-------------------------------------------------------------------------------
Errors
-------------------------------------------------------------------------------}
data AstDumpException =
HeaderNotFound
| ClangError
deriving stock (Show)
instance Exception AstDumpException where
displayException HeaderNotFound = "header not found"
displayException ClangError = "clang error"
{-------------------------------------------------------------------------------
Implementation
-------------------------------------------------------------------------------}
data DumpTrace =
DumpTraceResolveHeader ResolveHeaderMsg
| DumpTraceClang ClangMsg
deriving stock (Show, Generic)
deriving anyclass (PrettyForTrace, IsTrace Level)
clangAstDump :: Options -> IO ()
clangAstDump opts = do
putStrLn $ "## `" ++ opts.file.path ++ "`"
putStrLn ""
eitherRes <- withTracer tracerConf $ \tracer -> do
let clangArgs = clangArgsConfigToClangArgs cArgsConfig
tracerResolve = contramap DumpTraceResolveHeader tracer
tracerClang = contramap DumpTraceClang tracer
src <- maybe (throwIO HeaderNotFound) return . Map.lookup opts.file
=<< resolveHeaders tracerResolve clangArgs (Set.singleton opts.file)
let setup :: ClangSetup
setup = (defaultClangSetup clangArgs $ ClangInputFile (realPathToSourcePath src)) {
flags = cOpts
}
maybe (throwIO ClangError) return <=< withClang tracerClang setup $ \unit -> Just <$> do
rootCursor <- clang_getTranslationUnitCursor unit
void . HighLevel.clang_visitChildren rootCursor $ simpleFold $ \cursor -> do
loc <- clang_getPresumedLocation =<< clang_getCursorLocation cursor
case loc of
(file, _, _)
| opts.sameFile && file /= getRealPathText src -> foldContinue
| not opts.builtin && isBuiltIn file -> foldContinue
| otherwise -> foldDecls opts cursor
case eitherRes of
Left e -> do
print $ prettyForTrace e
exitFailure
Right _ -> pure ()
where
tracerConf :: TracerConfig Level DumpTrace
tracerConf = def {
verbosity = Verbosity Debug
}
cArgsConfig :: ClangArgsConfig FilePath
cArgsConfig = def {
extraIncludeDirs = opts.includePath
, argsInner = opts.clangArgsInner
}
cOpts :: BitfieldEnum CXTranslationUnit_Flags
cOpts = bitfieldEnum [
CXTranslationUnit_DetailedPreprocessingRecord
, CXTranslationUnit_IncludeAttributedTypes
, CXTranslationUnit_VisitImplicitAttributes
]
isBuiltIn :: Text -> Bool
isBuiltIn = (`elem` [T.pack "<built-in>", T.pack "<command line>"])
foldDecls :: Options -> CXCursor -> IO (Next IO ())
foldDecls opts = \cursor -> do
traceU_ 0 =<< clang_getCursorDisplayName cursor
dumpParents cursor
when opts.extents $ dumpExtent cursor
cursorKind <- clang_getCursorKind cursor
traceU 1 "cursor kind" cursorKind
isDecl <- clang_isDeclaration cursorKind
when opts.kind $
traceWhen 2 "declaration" id isDecl
cursorType <- clang_getCursorType cursor
isRecurse <- case fromSimpleEnum cursorKind of
Right CXCursor_StructDecl -> do
dumpType cursor cursorType isDecl
pure True
Right CXCursor_EnumDecl -> do
dumpType cursor cursorType isDecl
traceU 1 "integer type" =<< clang_getEnumDeclIntegerType cursor
pure True
Right CXCursor_FieldDecl -> do
dumpType cursor cursorType isDecl
traceU 1 "field offset" =<< clang_Cursor_getOffsetOfField cursor
isBitField <- clang_Cursor_isBitField cursor
when isBitField $
traceU 1 "bit width" =<< clang_getFieldDeclBitWidth cursor
pure False -- leaf
Right CXCursor_EnumConstantDecl -> do
dumpType cursor cursorType isDecl
traceU 1 "integer value" =<< clang_getEnumConstantDeclValue cursor
pure False -- leaf
Right CXCursor_FunctionDecl -> do
dumpType cursor cursorType isDecl
numArgs <- clang_getNumArgTypes cursorType
traceU 1 "args" numArgs
forM_ [0 .. numArgs - 1] $ \i -> do
argType <- clang_getArgType cursorType (fromIntegral i)
traceO_ 2 i =<< clang_getTypeSpelling argType
resultType <- clang_getResultType cursorType
traceU 1 "result" =<< clang_getTypeSpelling resultType
pure False
Right CXCursor_VarDecl -> do
traceU 1 "eval result" =<< HighLevel.clang_evaluate cursor
pure False
Right CXCursor_TypedefDecl -> do
dumpType cursor cursorType isDecl
traceU 1 "typedef name" =<< clang_getTypedefName cursorType
-- Results in repeated information unless the `typedef` is a
-- declaration.
pure True
Right CXCursor_MacroDefinition ->
-- TODO: Not defined yet.
pure True
Right CXCursor_InclusionDirective ->
pure False -- does not matter
Right CXCursor_UnionDecl -> do
dumpType cursor cursorType isDecl
pure True
Right CXCursor_LinkageSpec -> do
-- NOTE: The linkage specification has no display name, and so the
-- parents of the children are unknown, that is, the empty string "".
pure True
Right{} -> False <$ traceL 1 "CURSOR_KIND_NOT_IMPLEMENTED"
Left n -> False <$ traceU 1 "CURSOR_KIND_ENUM_OUT_OF_RANGE" n
when (isDecl && opts.comments) $ do
commentText <- clang_Cursor_getRawCommentText cursor
unless (T.null commentText) $ do
traceU 1 "comment" commentText
traceU 2 "brief" =<< clang_Cursor_getBriefCommentText cursor
dumpComment 2 Nothing =<< clang_Cursor_getParsedComment cursor
if isRecurse
then foldRecurse $ simpleFold (foldDecls opts)
else foldContinue
where
dumpParents :: CXCursor -> IO ()
dumpParents cursor = do
semanticParent <- clang_getCursorSemanticParent cursor
lexicalParent <- clang_getCursorLexicalParent cursor
parentsEq <- clang_equalCursors semanticParent lexicalParent
if parentsEq
then
traceU 1 "parent"
=<< clang_getCursorDisplayName semanticParent
else do
traceU 1 "semantic parent"
=<< clang_getCursorDisplayName semanticParent
traceU 1 "lexical parent"
=<< clang_getCursorDisplayName lexicalParent
dumpExtent :: CXCursor -> IO ()
dumpExtent cursor = do
extent <- clang_getCursorExtent cursor
(file, startLine, startCol) <-
clang_getPresumedLocation =<< clang_getRangeStart extent
(_, endLine, endCol) <-
clang_getPresumedLocation =<< clang_getRangeEnd extent
traceU 1 "extent" (file, (startLine, startCol), (endLine, endCol))
dumpType :: CXCursor -> CXType -> Bool -> IO ()
dumpType cursor cursorType isDecl = do
traceU 1 "cursor type" =<< clang_getTypeSpelling cursorType
when opts.typ $ do
let typeKind = cxtKind cursorType
traceU 2 "kind" $ fromSimpleEnum typeKind
traceU 3 "spelling" =<< clang_getTypeKindSpelling typeKind
traceU 2 "canonical"
=<< clang_getTypeSpelling =<< clang_getCanonicalType cursorType
when isDecl $ do
decl <- HighLevel.classifyDeclaration cursor
case decl of
Definition -> do
traceU 2 "declaration type" ("Definition" :: String)
traceU 2 "sizeof"
=<< handleCallFailed (clang_Type_getSizeOf cursorType)
traceU 2 "alignment"
=<< handleCallFailed (clang_Type_getAlignOf cursorType)
DefinitionElsewhere{} ->
traceU 2 "declaration type" ("DefinitionElsewhere" :: String)
DefinitionUnavailable ->
traceU 2 "declaration type" ("DefinitionUnavailable" :: String)
dumpComment :: Int -> Maybe CUInt -> CXComment -> IO ()
dumpComment level mIdx comment = do
commentKind <- clang_Comment_getKind comment
maybe (traceU level) (traceO level) mIdx "kind" $ fromSimpleEnum commentKind
traceWhen level1 "whitespace" id =<< clang_Comment_isWhitespace comment
case fromSimpleEnum commentKind of
Right CXComment_Null -> pure ()
Right CXComment_Text ->
traceU level1 "text" =<< clang_TextComment_getText comment
Right CXComment_InlineCommand -> do
traceU level1 "name"
=<< clang_InlineCommandComment_getCommandName comment
traceU level1 "render kind" . fromSimpleEnum
=<< clang_InlineCommandComment_getRenderKind comment
numArgs <- clang_InlineCommandComment_getNumArgs comment
when (numArgs > 0) $ do
traceU level1 "args" numArgs
forM_ [0 .. numArgs - 1] $ \i ->
traceO_ level2 i =<< clang_InlineCommandComment_getArgText comment i
Right CXComment_HTMLStartTag -> do
traceU level1 "name" =<< clang_HTMLTagComment_getTagName comment
traceWhen level1 "self-closing" id
=<< clang_HTMLStartTagComment_isSelfClosing comment
numAttrs <- clang_HTMLStartTag_getNumAttrs comment
when (numAttrs > 0) $ do
traceU level1 "attributes" numAttrs
forM_ [0 .. numAttrs - 1] $ \i -> do
traceO_ level2 i =<< clang_HTMLStartTag_getAttrName comment i
traceU level3 "value" =<< clang_HTMLStartTag_getAttrValue comment i
Right CXComment_HTMLEndTag -> do
traceU level1 "name" =<< clang_HTMLTagComment_getTagName comment
Right CXComment_Paragraph -> pure ()
Right CXComment_BlockCommand -> do
traceU level1 "name" =<<
clang_BlockCommandComment_getCommandName comment
numArgs <- clang_BlockCommandComment_getNumArgs comment
when (numArgs > 0) $ do
traceU level1 "args" numArgs
forM_ [0 .. numArgs - 1] $ \i ->
traceO_ level2 i =<< clang_BlockCommandComment_getArgText comment i
-- NOTE paragraph is also provided by children
-- para <- clang_BlockCommandComment_getParagraph comment
-- traceL level1 "paragraph"
-- dumpComment level2 Nothing para
Right CXComment_ParamCommand -> do
traceU level1 "name" =<< clang_ParamCommandComment_getParamName comment
traceU level1 "index"
=<< clang_ParamCommandComment_getParamIndex comment
traceUnless level2 "invalid" id
=<< clang_ParamCommandComment_isParamIndexValid comment
traceU level1 "direction" . fromSimpleEnum
=<< clang_ParamCommandComment_getDirection comment
traceU level2 "explicit"
=<< clang_ParamCommandComment_isDirectionExplicit comment
Right CXComment_TParamCommand -> do
traceU level1 "name" =<< clang_TParamCommandComment_getParamName comment
traceUnless level1 "position invalid" id
=<< clang_TParamCommandComment_isParamPositionValid comment
depth <- clang_TParamCommandComment_getDepth comment
traceU level1 "depth" depth
forM_ [0 .. depth] $ \i ->
traceO level2 i "index"
=<< clang_TParamCommandComment_getIndex comment i
Right CXComment_VerbatimBlockCommand -> pure ()
Right CXComment_VerbatimBlockLine ->
traceU level1 "text" =<< clang_VerbatimBlockLineComment_getText comment
Right CXComment_VerbatimLine ->
traceU level1 "text" =<< clang_VerbatimLineComment_getText comment
Right CXComment_FullComment -> do
traceUnless level "HTML" T.null =<< clang_FullComment_getAsHTML comment
traceUnless level "XML" T.null =<< clang_FullComment_getAsXML comment
Left n -> traceU level1 "COMMENT_KIND_ENUM_OUT_OF_RANGE" n
numChildren <- clang_Comment_getNumChildren comment
when (numChildren > 0) $ do
traceU level1 "children" numChildren
forM_ [0 .. numChildren - 1] $ \i ->
dumpComment level2 (Just i) =<< clang_Comment_getChild comment i
where
level1, level2, level3 :: Int
level1 = level + 1
level2 = level + 2
level3 = level + 3
{-------------------------------------------------------------------------------
Error handling
-------------------------------------------------------------------------------}
-- | Handle 'CallFailed' exceptions, returning the hint on error
handleCallFailed :: forall a m.
MonadIO m
=> IO a
-> m (Either String a)
handleCallFailed action = liftIO $ first getHint <$> try action
where
getHint :: CallFailed -> String
getHint (CallFailed hint _backtrace) = hint
{-------------------------------------------------------------------------------
Trace Functions
-------------------------------------------------------------------------------}
-- | Trace Markdown list item (internal)
--
-- This function is not meant to be used directly. Use one of the below
-- functions instead.
trace' :: (Show a, MonadIO m)
=> Int -- ^ indentation level, from 0
-> Maybe Int -- ^ index of ordered list
-> Maybe String -- ^ label
-> Maybe a -- ^ value
-> m ()
trace' level mIndex mLabel mValue = liftIO . putStrLn $ concat
[ replicate (level * 4) ' '
, maybe "* " ((++ ". ") . show) mIndex
, case (mLabel, mValue) of
(Just label, Just value) -> label ++ ": " ++ show value
(Just label, Nothing) -> label
(Nothing, Just value) -> show value
(Nothing, Nothing) -> ""
]
-- | Trace label (unordered)
traceL :: MonadIO m
=> Int -- ^ indentation level, from 0
-> String -- ^ label
-> m ()
traceL level label = trace' @() level Nothing (Just label) Nothing
-- | Trace value (unordered, no label)
traceU_ :: (Show a, MonadIO m)
=> Int -- ^ indentation level, from 0
-> a -- ^ value
-> m ()
traceU_ level value = trace' level Nothing Nothing (Just value)
-- | Trace value (unordered, with label)
traceU :: (Show a, MonadIO m)
=> Int -- ^ indentation level, from 0
-> String -- ^ label
-> a -- ^ value
-> m ()
traceU level label value = trace' level Nothing (Just label) (Just value)
-- | Trace value (ordered, no label)
traceO_ :: (Integral i, Show a, MonadIO m)
=> Int -- ^ indentation level, from 0
-> i -- ^ index of ordered list
-> a -- ^ value
-> m ()
traceO_ level index value =
trace' level (Just $ fromIntegral index) Nothing (Just value)
-- | Trace value (ordered, with label)
traceO :: (Integral i, Show a, MonadIO m)
=> Int -- ^ indentation level, from 0
-> i -- ^ index of ordered list
-> String -- ^ label
-> a -- ^ value
-> m ()
traceO level index label value =
trace' level (Just $ fromIntegral index) (Just label) (Just value)
-- | Trace value when predicate holds (unordered, with label)
traceWhen :: (Show a, MonadIO m)
=> Int -- ^ indentation level, from 0
-> String -- ^ label
-> (a -> Bool) -- ^ predicate
-> a -- ^ value
-> m ()
traceWhen level label p value = when (p value) $ traceU level label value
-- | Trace value when predicate does not hold (unordered, with label)
traceUnless :: (Show a, MonadIO m)
=> Int -- ^ indentation level, from 0
-> String -- ^ label
-> (a -> Bool) -- ^ predicate
-> a -- ^ value
-> m ()
traceUnless level label p value = unless (p value) $ traceU level label value
{-------------------------------------------------------------------------------
CLI
-------------------------------------------------------------------------------}
main :: IO ()
main = clangAstDump . uncurry applyAll =<< OA.execParser pinfo
where
pinfo :: OA.ParserInfo (Bool, Options)
pinfo = OA.info (OA.helper <*> parseOptions) $ mconcat
[ OA.fullDesc
, OA.progDesc "Clang AST dump"
, OA.failureCode 2
]
parseOptions :: OA.Parser (Bool, Options)
parseOptions = do
-- flags enabled by all flag
optComments <- mkFlag "comments" "show comments"
optExtents <- mkFlag "extents" "show extents"
optKind <- mkFlag "kind" "show kind details"
optType <- mkFlag "type" "show type details"
-- all flag
optAll <- mkFlag "all" "enable all above flags"
-- other options/arguments
optBuiltin <- mkFlag "builtin" "show builtin macros"
optSameFile <- mkFlag "same-file" "only show from specified file"
optIncludePath <- includeDirOptions
optClangArgsInner <- clangArgsInnerOption
optFile <- fileArgument
pure (optAll, Options{
builtin = optBuiltin
, comments = optComments
, extents = optExtents
, file = optFile
, kind = optKind
, includePath = optIncludePath
, clangArgsInner = optClangArgsInner
, sameFile = optSameFile
, typ = optType
})
includeDirOptions :: OA.Parser [FilePath]
includeDirOptions = OA.many . OA.strOption $ mconcat
[ OA.short 'I'
, OA.metavar "DIR"
, OA.help "Include search path directory"
]
clangArgsInnerOption :: OA.Parser [String]
clangArgsInnerOption = OA.many . OA.strOption $ mconcat [
OA.long "clang-option"
, OA.metavar "OPTION"
, OA.help "Pass option to clang"
]
fileArgument :: OA.Parser C.HashIncludeArg
fileArgument = fmap C.HashIncludeArg . OA.strArgument $ mconcat
[ OA.metavar "FILE"
, OA.help "C (header) file to parse"
]
mkFlag :: String -> String -> OA.Parser Bool
mkFlag flag doc = OA.switch $ OA.long flag <> OA.help doc
applyAll :: Bool -> Options -> Options
applyAll False opts = opts
applyAll True opts = opts {
comments = True
, extents = True
, kind = True
, typ = True
}