packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/TH/Internal.hs

module HsBindgen.TH.Internal (
    -- * Template Haskell API
    Config
  , IncludeDir(..)
  , withHsBindgenMacroLang
  , hashInclude
  , hashDefine
  , BindgenM

   -- * Internal artefacts
  , getExtensions
  , getThDecls
  ) where

import Control.Monad.State (State, execState)
import Control.Monad.State.Class (MonadState, modify)
import Data.Foldable qualified as Foldable
import Data.List qualified as List
import Data.Set qualified as Set
import Language.Haskell.TH qualified as TH
import Language.Haskell.TH.Syntax (getPackageRoot)
import System.FilePath (isAbsolute, (</>))

import Clang.CStandard
import Clang.Paths

import HsBindgen
import HsBindgen.Backend.Category
import HsBindgen.Backend.Extensions
import HsBindgen.Backend.Hs.CallConv
import HsBindgen.Backend.SHs.AST qualified as SHs
import HsBindgen.Backend.TH.Translation
import HsBindgen.Config
import HsBindgen.Config.Internal
import HsBindgen.Errors
import HsBindgen.Guasi
import HsBindgen.Imports
import HsBindgen.IR.C qualified as C
import HsBindgen.Language.Haskell qualified as Hs
import HsBindgen.Macro.Interface qualified as Macro
import HsBindgen.Macro.Type qualified as Macro
import HsBindgen.TraceMsg
import HsBindgen.Util.Tracer

{-------------------------------------------------------------------------------
  Template Haskell API
-------------------------------------------------------------------------------}

-- | Configuration with C include directories
--
-- C include directories can be provided relative to the package root (see the
-- 'IncludeDir' data constructor 'PkgDir').
type Config = Config_ IncludeDir

-- | C include directory added to the C include search path
data IncludeDir =
    -- | Include directory at absolute path
    --
    -- A relative path is warned about: it is resolved against the working
    -- directory of the compiler invocation, not the package root.
    AbsDir FilePath

    -- | Include directory relative to package root
    --
    -- An absolute path is an error: it would discard the package root.
  | PkgDir FilePath
  deriving stock (Eq, Show, Generic)

toFilePath :: FilePath -> IncludeDir -> FilePath
toFilePath root (PkgDir x) = root </> x
toFilePath _    (AbsDir x) = x

-- | Misuse of an 'IncludeDir' constructor
data IncludeDirMisuse =
    -- | 'AbsDir' with a relative path
    RelativeAbsDir FilePath

    -- | 'PkgDir' with an absolute path
  | AbsolutePkgDir FilePath
  deriving stock (Eq, Show)

checkIncludeDir :: IncludeDir -> Maybe IncludeDirMisuse
checkIncludeDir = \case
    AbsDir path | not (isAbsolute path) -> Just $ RelativeAbsDir path
    PkgDir path | isAbsolute path       -> Just $ AbsolutePkgDir path
    _otherwise                          -> Nothing

-- | An absolute 'PkgDir' path discards the package root, so it is never what
-- the user meant; a relative 'AbsDir' path does work, but only by accident.
isFatal :: IncludeDirMisuse -> Bool
isFatal RelativeAbsDir{}  = False
isFatal AbsolutePkgDir{}  = True

prettyIncludeDirMisuse :: IncludeDirMisuse -> String
prettyIncludeDirMisuse = \case
    RelativeAbsDir path -> unwords [
        "Relative path in 'AbsDir " ++ show path ++ "':"
      , "it is resolved relative to the working directory of the compiler"
      , "invocation, which is not necessarily the package root."
      , "Use an absolute path, or 'PkgDir' for a path relative to the"
      , "package root."
      ]
    AbsolutePkgDir path -> unwords [
        "Absolute path in 'PkgDir " ++ show path ++ "':"
      , "the package root is discarded, so the path is not relative to it."
      , "Use 'AbsDir' for an absolute path."
      ]

-- | Warn about, or reject, misuses of 'IncludeDir' constructors
--
-- NOTE: We report through Template Haskell instead of the tracer: the tracers
-- are only created in 'hsBindgenMacroLang', which needs the configuration we
-- are checking here. Consequently, the warning cannot be silenced via
-- 'ConfigTH'. The same applies to 'checkLanguageExtensions'.
checkIncludeDirs :: [IncludeDir] -> TH.Q ()
checkIncludeDirs includeDirs = do
    mapM_ (TH.reportWarning . prettyIncludeDirMisuse) warnings
    unless (null errors) $
      failQ $ List.intercalate "\n" $ map prettyIncludeDirMisuse errors
  where
    errors, warnings :: [IncludeDirMisuse]
    (errors, warnings) =
      List.partition isFatal $ mapMaybe checkIncludeDir includeDirs

-- | Generate bindings for given C headers at compile-time using a custom
-- macro language.
--
-- Use together with 'hashInclude', which acts in the 'TH.BindgenM' monad.
--
-- For example,
--
-- > withHsBindgenMacroLang myMacroLang def def $ do
-- >   hashDefine "FOO_FEATURE" "1"
-- >   hashInclude "foo.h"
-- >   hashInclude "bar.h"
withHsBindgenMacroLang ::
     forall l. Macro.HasTypes l
  => (ClangCStandard -> IO (Macro.Lang l))
     --  ^ The callback returning the macro 'Macro.Lang' to use.
  -> Config
  -> ConfigTH
  -> BindgenM ()
  -> TH.Q [TH.Dec]
withHsBindgenMacroLang mkMacroLang config configTH hashIncludes = do
    packageRoot <- getPackageRoot

    bindgenConfig <- toBindgenConfigTH config packageRoot configTH.categoryChoice

    let tracerConfigUnsafe :: TracerConfig Level TraceMsg
        tracerConfigUnsafe =
          tracerConfigDefTH
            & #verbosity      .~ configTH.verbosity
            & #customLogLevel .~ getCustomLogLevel configTH.customLogLevels

    let tracerConfigSafe :: TracerConfig SafeLevel SafeTraceMsg
        tracerConfigSafe =
          tracerConfigDefTH
            & #verbosity .~ configTH.verbosity

        -- Traverse root directives.
        bindgenState :: BindgenState
        bindgenState = execBindgenM hashIncludes (BindgenState [])

        -- Restore original order of the directives.
        uncheckedRootDirectives :: [C.UncheckedRootDirective]
        uncheckedRootDirectives = reverse bindgenState.rootDirectives

        artefact ::
          Artefact l
            ( [RealPath]
            , ( [C.RootDirective C.HashIncludeArg]
              , ([CWrapper], [SHs.SDecl])
              )
            )
        artefact = (,)
          <$> getDependencies
          <*> ((,) <$> RootDirectives <*> (Foldable.fold <$> FinalDecls))

    (deps, (rootDirectives, decls)) <- liftIO $ do
        hsBindgenMacroLang
          mkMacroLang
          tracerConfigUnsafe
          tracerConfigSafe
          bindgenConfig
          uncheckedRootDirectives
          artefact

    let fns  = bindgenConfig.frontend.fieldNamingStrategy
        exts = uncurry (getExtensions fns) decls
    checkLanguageExtensions exts
    -- Reverse SDecl order to counteract GHC reversing TH type/class
    -- declarations during dependency analysis, which causes Haddock to show
    -- declarations in reverse order.
    uncurry (getThDecls fns deps rootDirectives) (second reverse decls)

-- | @#include@ (i.e., generate bindings for) a C header
--
-- For example, the Haskell code,
--
-- > hashInclude "a.h"
--
-- corresponds to the following C code using angular brackets,
--
-- > #include <a.h>
--
-- See 'withHsBindgen'.
hashInclude :: FilePath -> BindgenM ()
hashInclude arg = do
    -- Prepend the directive to the list (the order will be reversed)
    modify $ #rootDirectives %~ (C.DirectiveHashInclude arg:)

-- | @#define@ a macro, in effect for all subsequent 'hashInclude's
--
-- > hashDefine "FOO" "1"    -- #define FOO 1
-- > hashDefine "FOO" ""     -- #define FOO      (empty replacement list)
-- > hashDefine "FOO(x)" "x" -- #define FOO(x) x
--
-- The directive applies to /all/ C stages: it is emitted both into the header
-- @libclang@ parses and into the generated C wrapper source GHC compiles.
--
-- Neither argument is validated; see t'C.HashDefine'.
--
-- This is @#define@ syntax, /not/ C ompiler @-D@ syntax. For example, @-D FOO@
-- corresponds to @--hash-define FOO 1@, translating to @#define FOO 1@.
hashDefine ::
     String -- ^ Macro name; may be function-like, e.g. @FOO(x)@
  -> String -- ^ Replacement list; @""@ for @#define FOO@
  -> BindgenM ()
hashDefine name value = do
    -- Prepend the directive to the list (the order will be reversed)
    modify $ #rootDirectives %~ (C.DirectiveHashDefine (C.HashDefine name value):)

{-------------------------------------------------------------------------------
  Internal artefacts
-------------------------------------------------------------------------------}

-- | Get required extensions
getExtensions :: FieldNamingStrategy -> [CWrapper] -> [SHs.SDecl] -> Set TH.Extension
getExtensions fieldNaming wrappers decls =
    userlandCapiExt <> foldMap (requiredExtensions fieldNaming) decls
  where
    userlandCapiExt = case wrappers of
      []  -> Set.empty
      _xs -> Set.singleton TH.TemplateHaskell

-- | Get Template Haskell declarations
--
-- Internal!
--
-- Non-IO part of 'withHsBindgen'.
getThDecls
    :: Guasi q
    => FieldNamingStrategy
    -> [RealPath]
    -> [C.RootDirective C.HashIncludeArg]
    -> [CWrapper]
    -> [SHs.SDecl]
    -> q [TH.Dec]
getThDecls fns deps rootDirectives wrappers decls = do
    -- Record dependencies, including transitively included headers.
    mapM_ (addDependentFile . getRealPath) deps

    -- Add userland-CAPI wrappers source code.
    unless (null wrapperSrc) $ addCSource wrapperSrc

    -- Generate TH declarations.
    xs <- fmap concat $ traverse (mkDecl fns) decls

    -- Report warnings/errors.
    reportMissingModules

    pure xs
  where
    wrapperSrc :: String
    wrapperSrc = getCWrappersSource rootDirectives wrappers

    reportMissingModules :: Guasi q => q ()
    reportMissingModules = do
      missingModules <- (.missingModules) <$> getGuasi
      unless (Set.null missingModules) $ do
        reportError $ concat $
          "External types not in scope. Add the following import(s):\n"
          : map getImportStatement (Set.elems missingModules)

    getImportStatement :: Hs.ModuleName -> String
    getImportStatement m =
      "    import " <> Hs.moduleNameToString m <> " qualified"

{-------------------------------------------------------------------------------
  Helpers
-------------------------------------------------------------------------------}

-- | The default tracer configuration in Q uses 'outputConfigTH'
tracerConfigDefTH :: TracerConfig l a
tracerConfigDefTH = def{outputConfig = outputConfigTH}

-- | State monad used by 'HsBindgen.withBindgen'
newtype BindgenM a = BindgenM { unwrap :: State BindgenState a }
  deriving newtype (Functor, Applicative, Monad, MonadState BindgenState)

execBindgenM :: BindgenM a -> BindgenState -> BindgenState
execBindgenM = execState . (.unwrap)

-- | State manipulated by 'hashInclude' and 'hashDefine'
--
-- Internal!
data BindgenState = BindgenState {
      rootDirectives :: [C.UncheckedRootDirective]
    }
  deriving stock (Generic)

-- NOTE: We could also check which enabled extension may interfere with the
-- generated code (e.g. Strict/Data).
--
-- NOTE: We report through Template Haskell instead of the tracer; see
-- 'checkIncludeDirs'.
checkLanguageExtensions :: Set TH.Extension -> TH.Q ()
checkLanguageExtensions requiredExts = do
    enabledExts <- Set.fromList <$> extsEnabled
    let missingExts  = requiredExts `Set.difference` enabledExts

    unless (null missingExts) $ do
      failQ $ unlines $
        "Missing language extension(s): " :
          (map (("    - " ++) . show) (toList missingExts))

toBindgenConfigTH :: Config -> FilePath -> ByCategory Choice -> TH.Q BindgenConfig
toBindgenConfigTH config packageRoot choice = do
    checkIncludeDirs $ Foldable.toList config
    uniqueId <- getUniqueId
    hsModuleName <- fromString . TH.loc_module <$> TH.location
    let bindgenConfig :: BindgenConfig
        bindgenConfig =
          toBindgenConfig
            (toFilePath packageRoot <$> config)
            uniqueId
            hsModuleName
            choice
    pure bindgenConfig
  where
    getUniqueId :: TH.Q UniqueId
    getUniqueId = UniqueId . TH.loc_package <$> TH.location