packages feed

hs-bindgen-1.0.0.0: app/HsBindgen/Cli/Info/BuiltinMacros.hs

-- | @hs-bindgen-cli info builtin-macros@ command
--
-- This command dumps builtin macros using @libclang@.  The same can be done
-- using @clang@ with command @clang -dM -E -x c /dev/null@.  This command
-- exists because @libclang@ exposes some different macros than @clang@.
--
-- This command is different from the @clang@ dump command in that it fully
-- expands macros.
--
-- Intended for qualified import.
--
-- > import HsBindgen.Cli.Info.BuiltinMacros qualified as BuiltinMacros
module HsBindgen.Cli.Info.BuiltinMacros (
    -- * CLI help
    info
    -- * Options
  , Opts(..)
  , parseOpts
    -- * Execution
  , exec
  ) where

import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as Text
import Options.Applicative hiding (info)

import Clang.Args
import Clang.Enum.Simple
import Clang.HighLevel qualified as HighLevel
import Clang.HighLevel.Types
import Clang.LowLevel.Core

import HsBindgen.App
import HsBindgen.Boot
import HsBindgen.Clang
import HsBindgen.Config.ClangArgs
import HsBindgen.Frontend.Pass.Parse.Builtin
import HsBindgen.Imports
import HsBindgen.IR.C qualified as C
import HsBindgen.TraceMsg
import HsBindgen.Util.Tracer

{-------------------------------------------------------------------------------
  CLI help
-------------------------------------------------------------------------------}

info :: InfoMod a
info = progDesc "List LLVM/Clang builtin macros"

{-------------------------------------------------------------------------------
  Options
-------------------------------------------------------------------------------}

newtype Opts = Opts {
      clangArgsConfig :: ClangArgsConfig FilePath
    }

parseOpts :: Parser Opts
parseOpts = Opts <$> parseClangArgsConfig

{-------------------------------------------------------------------------------
  Execution
-------------------------------------------------------------------------------}

exec :: GlobalOpts -> Opts -> IO ()
exec global opts =
    void . withTracer global.unsafe $ \tracer -> do
      let tracerBoot  = contramap TraceBoot tracer
          tracerClang = contramap (TraceFrontend . FrontendClang) tracer
      clangArgs <-
        (.clangArgs) <$> getClangArtefacts tracerBoot opts.clangArgsConfig
      -- 1. Get the names of the builtin macros
      names <- getBuiltinMacroNames tracerClang clangArgs
      -- 2. Try to get stringified values for all macros
      results1 <- getMacros tracerClang clangArgs names
      -- 3. Find failed macros, where the stringified macro is the macro name,
      --    and remove them from the results
      let failedNames =
            [n | (n, v) <- Map.toList results1, Text.unpack n == v]
          results2 = foldr Map.delete results1 failedNames
      -- 4. Try to get stringified values for the failed macros by passing a
      --    single paramater
      paramResults <- getParamMacros tracerClang clangArgs failedNames
      -- 5. Compute the final results and the set of all names, with the
      --    parameterized macro names
      let results3 = results2 <> paramResults
          paramNamesSet = Map.keysSet paramResults
          namesSet =
            Set.difference
              (Set.fromList names <> paramNamesSet)
              (Set.map stripParams paramNamesSet)
      -- 6. Print all macros, including any failed ones
      forM_ (Set.toAscList namesSet) $ \name ->
        putStrLn $ renderResult name (Map.lookup name results3)
  where
    -- Output space even when @v@ is empty to match @clang@ behavior
    renderResult :: Text -> Maybe String -> String
    renderResult builtinName mValue =
      let n = Text.unpack builtinName
          v = fromMaybe "UNKNOWN_MACRO_DEFINITION" mValue
      in  "#define " ++ n ++ ' ' : v

    stripParams :: Text -> Text
    stripParams = Text.takeWhile (/= '(')

-- | Get the names of all builtin macros
--
-- Parameterized macros do /not/ include the parameters.
getBuiltinMacroNames :: Tracer ClangMsg -> ClangArgs -> IO [Text]
getBuiltinMacroNames tracer clangArgs =
    fmap (fromMaybe []) . withClang' tracer setup $ \unit -> do
      root <- clang_getTranslationUnitCursor unit
      Just <$> HighLevel.clang_visitChildren root visit
  where
    setup :: ClangSetup
    setup = defaultClangSetup clangArgs $
      ClangInputMemory "hs-bindgen-builtins.h" ""

    visit :: Fold IO Text
    visit = simpleFold $ \curr -> do
      mBuiltin <- checkIsBuiltin curr
      case mBuiltin of
        Just name -> foldContinueWith name
        Nothing   -> foldBreak

-- | Get stringified definitions of the specified macros
--
-- The stringified definition of a parameteried macro is the macro name itself
-- (not including the parameters).
getMacros :: Tracer ClangMsg -> ClangArgs -> [Text] -> IO (Map Text String)
getMacros tracer clangArgs names =
    fmap (maybe Map.empty Map.fromList) . withClang' tracer setup $ \unit -> do
      root <- clang_getTranslationUnitCursor unit
      Just <$> HighLevel.clang_visitChildren root visit
  where
    setup :: ClangSetup
    setup =
        defaultClangSetup clangArgs
      . ClangInputMemory "hs-bindgen-builtins.h"
      . unlines
      $ helperMacros ++ map mkDecl names

    -- https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html
    helperMacros :: [String]
    helperMacros = [
        "#define STREX(x) #x"
      , "#define STR(x) STREX(x)"
      ]

    mkDecl :: Text -> String
    mkDecl builtinName =
      let name = Text.unpack builtinName
      in  "const char *BUILTIN_X_" ++ name ++ " = STR(" ++ name ++ ");"

    parseName :: Text -> Maybe Text
    parseName = Text.stripPrefix "BUILTIN_X_"

    visit :: Fold IO (Text, String)
    visit = simpleFold $ \curr -> do
      C.prelimDeclIdAtCursor curr C.NameKindOrdinary >>= \case
        C.PrelimDeclIdUnnamed{}  -> foldContinue
        C.PrelimDeclIdNamed name ->
          (fromSimpleEnum <$> clang_getCursorKind curr) >>= \case
            Right CXCursor_VarDecl -> case parseName name.text of
              Nothing          -> foldContinue
              Just builtinName -> HighLevel.clang_evaluate curr >>= \case
                Just (EvalResultString s) -> foldContinueWith (builtinName, s)
                _otherwise                -> foldContinue
            _otherwise -> foldContinue

-- | Get stringified definitions of the specified macros with a single parameter
--
-- This is a dirty hack just for the builtin macros.  It just passes a single
-- parameter and expects the stringified result to either be unchanged for
-- append some string.
getParamMacros ::
     Tracer ClangMsg
  -> ClangArgs
  -> [Text]
  -> IO (Map Text String)
getParamMacros tracer clangArgs names =
    fmap (maybe Map.empty Map.fromList) . withClang' tracer setup $ \unit -> do
      root <- clang_getTranslationUnitCursor unit
      Just <$> HighLevel.clang_visitChildren root visit
  where
    setup :: ClangSetup
    setup =
        defaultClangSetup clangArgs
      . ClangInputMemory "hs-bindgen-builtins.h"
      . unlines
      $ helperMacros ++ map mkDecl names

    -- https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html
    helperMacros :: [String]
    helperMacros = [
        "#define STREX(x) #x"
      , "#define STR(x) STREX(x)"
      ]

    mkDecl :: Text -> String
    mkDecl builtinName =
      let name = Text.unpack builtinName
      in  "const char *BUILTIN_X_" ++ name ++ " = STR(" ++ name ++ "(c));"

    parseName :: Text -> Maybe Text
    parseName = Text.stripPrefix "BUILTIN_X_"

    visit :: Fold IO (Text, String)
    visit = simpleFold $ \curr ->
      C.prelimDeclIdAtCursor curr C.NameKindOrdinary >>= \case
        C.PrelimDeclIdUnnamed{}  -> foldContinue
        C.PrelimDeclIdNamed name ->
          (fromSimpleEnum <$> clang_getCursorKind curr) >>= \case
            Right CXCursor_VarDecl -> case parseName name.text of
              Nothing          -> foldContinue
              Just builtinName -> HighLevel.clang_evaluate curr >>= \case
                Just (EvalResultString s) -> case mangleValue s of
                  Just s' -> foldContinueWith (mangleName builtinName, s')
                  Nothing -> foldContinue
                _otherwise -> foldContinue
            _otherwise -> foldContinue

    mangleName :: Text -> Text
    mangleName = (<> "(c)")

    mangleValue :: String -> Maybe String
    mangleValue = \case
      "c"        -> Just "c"
      'c' : s    -> Just ("c##" ++ s)
      _otherwise -> Nothing