packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/Frontend.hs

module HsBindgen.Frontend (
    runFrontend
  , FrontendArtefact (..)
  , FrontendMsg(..)
  , ParseInfo(..)
  ) where


import Prelude hiding (zip)

import Control.Exception (catch)
import Data.List.NonEmpty qualified as NE
import Data.Map.Lazy qualified as Map

import Clang.Enum.Bitfield
import Clang.LowLevel.Core
import Clang.Paths

import HsBindgen.Boot
import HsBindgen.Cache
import HsBindgen.Clang
import HsBindgen.Config.Internal
import HsBindgen.Doxygen
import HsBindgen.Frontend.Analysis.IncludeGraph (IncludeGraph)
import HsBindgen.Frontend.Analysis.UnnamedIdUsage (UnnamedIdUsageAnalysis)
import HsBindgen.Frontend.Analysis.UnnamedIdUsage qualified as UnnamedIdUsageAnalysis
import HsBindgen.Frontend.Pass.AdjustTypes
import HsBindgen.Frontend.Pass.AdjustTypes.IsPass
import HsBindgen.Frontend.Pass.ConstructTranslationUnit
import HsBindgen.Frontend.Pass.ConstructTranslationUnit.IsPass
import HsBindgen.Frontend.Pass.EnrichComments
import HsBindgen.Frontend.Pass.EnrichComments.IsPass
import HsBindgen.Frontend.Pass.FillUnnamedIds
import HsBindgen.Frontend.Pass.FillUnnamedIds.IsPass
import HsBindgen.Frontend.Pass.Final
import HsBindgen.Frontend.Pass.MangleNames
import HsBindgen.Frontend.Pass.MangleNames.IsPass
import HsBindgen.Frontend.Pass.Parse
import HsBindgen.Frontend.Pass.Parse.IsPass
import HsBindgen.Frontend.Pass.Parse.Monad.Decl qualified as ParseDecl
import HsBindgen.Frontend.Pass.Parse.Result
import HsBindgen.Frontend.Pass.PrepareReparse
import HsBindgen.Frontend.Pass.PrepareReparse.IsPass
import HsBindgen.Frontend.Pass.ReparseMacroExpansions
import HsBindgen.Frontend.Pass.ReparseMacroExpansions.IsPass
import HsBindgen.Frontend.Pass.ResolveBindingSpecs
import HsBindgen.Frontend.Pass.ResolveBindingSpecs.IsPass
import HsBindgen.Frontend.Pass.Select
import HsBindgen.Frontend.Pass.Select.IsPass
import HsBindgen.Frontend.Pass.SimplifyAST
import HsBindgen.Frontend.Pass.SimplifyAST.IsPass
import HsBindgen.Frontend.Pass.TranslateTypes
import HsBindgen.Frontend.Pass.TranslateTypes.IsPass
import HsBindgen.Frontend.Pass.TypecheckMacros
import HsBindgen.Frontend.Pass.TypecheckMacros.IsPass
import HsBindgen.Frontend.Predicate
import HsBindgen.Frontend.ProcessIncludes
import HsBindgen.Frontend.RootHeader (RootHeader, RootHeaderMsg)
import HsBindgen.Frontend.RootHeader qualified as RootHeader
import HsBindgen.Frontend.TranslationUnit qualified as C
import HsBindgen.Imports
import HsBindgen.IR.C qualified as C
import HsBindgen.IR.Pass
import HsBindgen.Language.Haskell qualified as Hs
import HsBindgen.Macro.Type qualified as Macro
import HsBindgen.Macro.UniqueExpansion qualified as UniqueExpansion
import HsBindgen.Util.Tracer

import Doxygen.Parser (Doxygen, DoxygenException (..), Result (..),
                       emptyDoxygen, parse)

-- | Frontend
--
-- = Overview of passes
--
-- See the documentation of 'HsBindgen.Frontend.Pass.IsPass'.
--
-- == 1. "HsBindgen.Frontend.Pass.Parse"
--
-- "HsBindgen.Frontend.Pass.Parse" traverses the @libclang@ AST, getting all
-- information that we need from @libclang@ and constructing a pure Haskell
-- representation (see "HsBindgen.Frontend.AST.Decl"). It runs in 'IO', to
-- interface with @libclang@.
--
-- Constraints:
--
-- * Must be first, to get the declarations from @libclang@
--
-- == 2. "HsBindgen.Frontend.Pass.SimplifyAST"
--
-- "HsBindgen.Frontend.Pass.SimplifyAST" simplifies the AST by converting
-- untagged enums without use sites into pattern synonym declarations. For
-- example, @enum { FOO, BAR }@ is converted into separate pattern synonym
-- declarations that will later be rendered as Haskell pattern synonyms.
--
-- Constraints:
--
-- * Must run before "HsBindgen.Frontend.Pass.FillUnnamedIds" because
--   FillUnnamedIds will delete unnamed declarations without use sites (it
--   needs a use site to determine a name)."
--
-- == 3. "HsBindgen.Frontend.Pass.FillUnnamedIds"
--
-- "HsBindgen.Frontend.Pass.FillUnnamedIds" assigns names to all unnamed
-- declarations, replacing 'HsBindgen.Frontend.Naming.PrelimDeclId' by
-- 'HsBindgen.Frontend.Naming.DeclId', which is used from this point forward.
--
-- Constraints:
--
-- * Must be before "HsBindgen.Frontend.Pass.ConstructTranslationUnit" so that
--   'HsBindgen.Frontend.Naming.DeclId' can be used as the key for the
--   'DeclIndex.DeclIndex', 'UseDeclGraph.UseDeclGraph', and
--   'DeclUseGraph.DeclUseGraph'
-- * Must be before "HsBindgen.Frontend.Pass.ResolveBindingSpecs" so that
--   binding specifications can use the assigned names
--
-- == 4. "HsBindgen.Frontend.Pass.EnrichComments"
--
-- "HsBindgen.Frontend.Pass.EnrichComments" enriches parsed declarations with
-- doxygen comments by looking up each declaration in the 'Doxygen' state.
--
-- Constraints:
--
-- * Must be after "HsBindgen.Frontend.Pass.FillUnnamedIds" so that
--   'HsBindgen.Frontend.Naming.DeclId' is available for the 'DeclIndex' and
--   for building doxygen-qualified names
--
-- == 5. "HsBindgen.Frontend.Pass.ConstructTranslationUnit"
--
-- "HsBindgen.Frontend.Pass.ConstructTranslationUnit" constructs a list of
-- sorted declarations as well as 'DeclIndex.DeclIndex',
-- 'UseDeclGraph.UseDeclGraph', and 'DeclUseGraph.DeclUseGraph'.
--
-- Constraints:
--
-- * Must be before the rest of the passes because they use these structures and
--   depend on the ordering of declarations
--
-- == 6. "HsBindgen.Frontend.Pass.TypecheckMacros"
--
-- "HsBindgen.Frontend.Pass.TypecheckMacros" collects known types and typechecks
-- all macros. Note that macros may neither refer to nor introduce new unnamed
-- declarations, so running "HsBindgen.Frontend.Pass.FillUnnamedIds" before
-- "HsBindgen.Frontend.Pass.TypecheckMacros" is fine.
--
-- Constraints:
--
-- * Must be before "HsBindgen.Frontend.Pass.ReparseMacroExpansions", because
--   "HsBindgen.Frontend.Pass.TypecheckMacros" typechecks macro-defined types
--   that are required to parse declarations with macro expansions.
--
-- == 7. "HsBindgen.Frontend.Pass.PrepareReparse"
--
-- @PrepareReparse@ prepares declarations that need to be reparsed because they
-- contain macro invocations. These declarations carry raw tokens intended to be
-- reparsed later, and the main goal of @PrepareReparse@ is to preprocess the
-- tokens to expand select macro invocations. If we leave macro invocations
-- unexpanded, then declarations that contain them will very likely fail to be
-- reparsed. In particular this is because the reparser expects its input to be
-- preprocessed. However, we do not expand a macro invocation if its definition
-- is identified (through parsing and typechecking) as a macro-defined type.
-- Such definitions are instead treated by the reparser as if they are
-- references to actual typedefs.
--
-- See issue #1225 for examples of how /not/ preprocessing caused problems in
-- the past:
--
-- <https://github.com/well-typed/hs-bindgen/issues/1225>
--
-- Constraints:
--
-- * Must be after @TypecheckMacros@, because @PrepareReparse@ needs to know
--   which macro definitions where identified as macro-defined types before we
--   can decide whether to expand macro invocations.
--
-- * Must be before @ReparseMacroExpansions@, because @ReparseMacroExpansions@
--   expects its reparser input to be preprocessed, which is what
--   @PrepareReparse@ takes care of.
--
-- == 8. "HsBindgen.Frontend.Pass.ReparseMacroExpansions"
--
-- "HsBindgen.Frontend.Pass.ReparseMacroExpansions" reparses declarations that
-- contain macro expansions.
--
-- Constraints:
--
-- * Must be before "HsBindgen.Frontend.Pass.AdjustTypes", because
--   "HsBindgen.Frontend.Pass.ReparseMacroExpansions" reparses declarations
--   referencing macro-defined types that may have to be adjusted.
--
-- == 9. "HsBindgen.Frontend.Pass.ResolveBindingSpecs"
--
-- "HsBindgen.Frontend.Pass.ResolveBindingSpecs" has two responsibilities:
--
-- * It matches declarations/uses with external binding specifications, removing
--   matching declarations and replacing uses with external references.
-- * It matches declarations with prescriptive binding specifications, either
--   omitting them or annotating the declarations with specifications to be used
--   in later passes.
--
-- Constraints:
--
-- * Must be before "HsBindgen.Frontend.Pass.Select" because prescriptive
--   binding specs may omit declarations and external binding specs may remove
--   declarations, and program slicing must take this into account
-- * Must be before "HsBindgen.Frontend.Pass.MangleNames" because prescriptive
--   binding specs may configure typedef squashing, which happens in
--   MangleNames.
-- * Must be before "HsBindgen.Frontend.Pass.MangleNames" because prescriptive
--   binding specs may specify arbitrary names
--
-- == 10. "HsBindgen.Frontend.Pass.MangleNames"
--
-- "HsBindgen.Frontend.Pass.MangleNames" assigns Haskell names for types,
-- constructors, fields, etc. It also deals with name clashes that can arise
-- from typedefs, squashing "unneeded" typedefs.
--
-- == 11. "HsBindgen.Frontend.Pass.AdjustTypes"
--
-- "HsBindgen.Frontend.Pass.AdjustTypes" adjusts types in declarations. For
-- example, if a function argument is a function type, then it is adjusted to a
-- function /pointer/ type.
--
-- == 12. "HsBindgen.Frontend.Pass.TranslateTypes"
--
-- "HsBindgen.Frontend.Pass.TranslateTypes" translates types (use sites) to
-- Haskell types.
--
-- == 13. "HsBindgen.Frontend.Pass.Select"
--
-- "HsBindgen.Frontend.Pass.Select" filters the declarations using predicates
-- and program slicing. It also emits delayed trace messages for declarations
-- that are selected.
--
-- Constraints:
--
-- * The 'Select' pass must come last so that if a declaration is
--   'HsBindgen.Frontend.Analysis.DeclIndex.UnusableEntry' for whatever reason
--   (e.g., it contains unsupported types such as @long double@, or the name
--   mangler was unable to find a suitable name, etc.), the 'Select' pass can
--   make sure that the unusable declaration /and all of its dependencies/ will
--   not be selected.
runFrontend ::
     forall l. (Macro.HasTypes l, HasCallStack)
  => Tracer FrontendMsg
  -> FrontendConfig
  -> BootArtefact l
  -> IO (FrontendArtefact l)
runFrontend tracer config boot = do
    rootHeaderC <- cache "rootHeader" $ do
      (msgs, rootHeader) <- RootHeader.fromRootDirectives <$> boot.rootDirectives
      liftIO $ mapM_ (traceWith tracerRootHeader . withCallStack) msgs
      pure rootHeader

    parsePass <- cache "parse" $ do
      setup     <- getSetup =<< rootHeaderC
      macroLang <- boot.macroLang
      liftIO $ withClang (contramap FrontendClang tracer) setup $ \unit -> do
        (includeGraph, isMainHeader, isInMainHeaderDir, getMainHeadersAndInclude, mainHeaderPaths) <-
          processIncludes unit
        -- Run doxygen on the resolved main header paths to extract
        -- structured comments.  The paths come from clang's own include
        -- resolution (via processIncludes), so no separate path search
        -- is needed.
        let resolvedPaths = map getRealPath mainHeaderPaths
            emptyResult   = Result {
                doxygen = emptyDoxygen, warnings = [], doxygenVersion = "unknown"
              }
        doxyResult <- case NE.nonEmpty resolvedPaths of
          Nothing -> pure emptyResult
          Just paths -> parse config.doxygenConfig paths
            `catch` \(e :: DoxygenException) -> do
              traceWith tracer $ withCallStack $ FrontendDoxygen $ DoxygenWarning e
              pure emptyResult

        -- Emit structured warnings for unsupported content
        forM_ doxyResult.warnings $ \w ->
          traceWith tracer $ withCallStack $ FrontendDoxygen $ DoxygenUnsupported w

        let parseEnv :: ParseDecl.Env
            parseEnv = ParseDecl.Env{
                unit                     = unit
              , getMainHeadersAndInclude = getMainHeadersAndInclude
              , emptyMacros              = config.emptyMacros
              , tracer                   = contramap FrontendParse tracer
              }
        (parseResults, macroAnalysis) <- parseDecls macroLang parseEnv

        let decls :: [C.Decl l Parse]
            decls = mapMaybe getParseResultMaybeDecl parseResults
            usageAnalysis = UnnamedIdUsageAnalysis.fromDecls decls

        pure $ ParsePassResult {
            results           = parseResults
          , doxygen           = doxyResult.doxygen
          , includeGraph      = includeGraph
          , isMainHeader      = isMainHeader
          , isInMainHeaderDir = isInMainHeaderDir
          , getMainHeaders    = toGetMainHeaders getMainHeadersAndInclude
          , usageAnalysis     = usageAnalysis
          , macroAnalysis     = macroAnalysis
          }

    parseMeta <- cache "parseMeta" $ do
      afterParse <- parsePass
      pure ParseInfo {
        includeGraph   = afterParse.includeGraph
      , getMainHeaders = afterParse.getMainHeaders
      }

    simplifyASTPass <- cache "simplifyAST" $ do
      afterParse <- parsePass
      let (afterSimplifyAST, msgsSimplifyAST) =
            simplifyAST afterParse.usageAnalysis afterParse.results
      forM_ msgsSimplifyAST $ traceWith (contramap FrontendSimplifyAST tracer)
      pure afterSimplifyAST

    fillUnnamedIdsPass <- cache "fillUnnamedIds" $ do
      afterParse       <- parsePass
      afterSimplifyAST <- simplifyASTPass
      let (afterFillUnnamedIds, msgsFillUnnamedIds) =
            fillUnnamedIds afterParse.usageAnalysis afterSimplifyAST
      forM_ msgsFillUnnamedIds $ traceWith (contramap FrontendFillUnnamedIds tracer)
      pure afterFillUnnamedIds

    enrichCommentsPass <- cache "enrichComments" $ do
      afterParse         <- parsePass
      afterFillUnnamedIds <- fillUnnamedIdsPass
      pure $ enrichComments afterParse.doxygen afterFillUnnamedIds

    constructTranslationUnitPass <- cache "constructTranslationUnit" $ do
      macroLang  <- boot.macroLang
      afterParse <- parsePass
      afterEnrichComments <- enrichCommentsPass
      let afterConstructTranslationUnit =
            constructTranslationUnit
              macroLang
              afterParse.macroAnalysis
              afterParse.isMainHeader
              afterEnrichComments
              afterParse.includeGraph
      pure afterConstructTranslationUnit

    typecheckMacrosPass <- cache "typecheckMacros" $ do
      macroLang                     <- boot.macroLang
      afterConstructTranslationUnit <- constructTranslationUnitPass
      pure $ typecheckMacros macroLang afterConstructTranslationUnit

    prepareReparsePass <- cache "prepareReparse" $ do
      (afterTypecheckMacros, _, _) <- typecheckMacrosPass
      clangExe <- boot.clangExe
      rootHeader <- rootHeaderC
      setup <- getSetup rootHeader
      liftIO $ prepareReparse
                  (contramap FrontendPrepareReparse tracer)
                  clangExe
                  setup
                  rootHeader
                  afterTypecheckMacros

    reparseMacroExpansionsPass <- cache "reparseMacroExpansions" $ do
      (_, knownTypes, knownMacros) <- typecheckMacrosPass
      afterPrepareReparse <- prepareReparsePass
      cStd <- boot.cStandard
      pure $ reparseMacroExpansions cStd (Map.map coercePass knownTypes) knownMacros afterPrepareReparse

    resolveBindingSpecsPass <- cache "resolveBindingSpecs" $ do
      afterReparseMacroExpansions <- reparseMacroExpansionsPass
      extSpecs <- boot.externalBindingSpecs
      pSpec    <- boot.prescriptiveBindingSpec
      let moduleName = Hs.ModuleName boot.baseModule.text -- do not import Backend
          (afterResolveBindingSpecs, msgsResolveBindingSpecs) =
            resolveBindingSpecs moduleName extSpecs pSpec afterReparseMacroExpansions
      forM_ msgsResolveBindingSpecs $ traceWith (contramap FrontendResolveBindingSpecs tracer)
      pure afterResolveBindingSpecs

    mangleNamesPass <- cache "mangleNames" $ do
      afterResolveBindingSpecs <- resolveBindingSpecsPass
      let (afterMangleNames, msgsMangleNames) =
            mangleNames config.fieldNamingStrategy afterResolveBindingSpecs
      forM_ msgsMangleNames $ traceWith (contramap FrontendMangleNames tracer)
      pure afterMangleNames

    adjustTypesPass <- cache "AdjustTypes" $ do
      afterMangleNamesPass <- mangleNamesPass
      pure $ adjustTypes afterMangleNamesPass

    translateTypesPass <- cache "TranslateTypes" $ do
      afterAdjustTypesPass <- adjustTypesPass
      let (afterTranslateTypes, msgsTranslateTypes) =
            translateTypes afterAdjustTypesPass
      forM_ msgsTranslateTypes $ traceWith (contramap FrontendTranslateTypes tracer)
      pure afterTranslateTypes

    selectPass <- cache "select" $ do
      afterParse <- parsePass
      afterTranslateTypesPass <- translateTypesPass
      let (afterSelect, msgsSelect) =
            selectDecls
              afterParse.isMainHeader
              afterParse.isInMainHeaderDir
              selectConfig
              afterTranslateTypesPass
      forM_ msgsSelect $  traceWith (contramap FrontendSelect tracer)
      pure afterSelect

    finalPass <- cache "Final" $ selectPass

    pure FrontendArtefact{
        parseMeta                = parseMeta
      , parse                    = (.results) <$> parsePass
      , doxygen                  = (.doxygen) <$> parsePass
      , simplifyAST              = simplifyASTPass
      , fillUnnamedIds           = fillUnnamedIdsPass
      , enrichComments           = enrichCommentsPass
      , constructTranslationUnit = constructTranslationUnitPass
      , typecheckMacros          = (\(x,_,_) -> x) <$> typecheckMacrosPass
      , reparseMacroExpansions   = reparseMacroExpansionsPass
      , resolveBindingSpecs      = resolveBindingSpecsPass
      , mangleNames              = mangleNamesPass
      , adjustTypes              = adjustTypesPass
      , translateTypes           = translateTypesPass
      , select                   = selectPass
      , final                    = finalPass
      }
  where
    getSetup :: RootHeader -> Cached ClangSetup
    getSetup rootHeader = do
      clangArgs <- boot.clangArgs
      let hContent = RootHeader.content rootHeader
          setup = defaultClangSetup clangArgs $ ClangInputMemory hFilePath hContent
      pure $ setup {
          flags = bitfieldEnum [
              CXTranslationUnit_DetailedPreprocessingRecord
            , CXTranslationUnit_IncludeAttributedTypes
            , CXTranslationUnit_VisitImplicitAttributes
            ]
        }

    hFilePath :: FilePath
    hFilePath = getSourcePath RootHeader.name

    selectConfig :: SelectConfig
    selectConfig = SelectConfig{
          programSlicing     = config.programSlicing
        , selectionPredicate = config.selectionPredicate
        }

    cache :: String -> Cached a -> IO (Cached a)
    cache = cacheWith (contramap (FrontendCache . SafeTrace) tracer) . Just

    tracerRootHeader :: Tracer RootHeaderMsg
    tracerRootHeader = contramap FrontendRootHeader tracer

{-------------------------------------------------------------------------------
  Artefact
-------------------------------------------------------------------------------}

data FrontendArtefact l = FrontendArtefact {
      parseMeta                :: Cached ParseInfo
    , parse                    :: Cached [ParseResult       l Parse]
    , doxygen                  :: Cached Doxygen
    , simplifyAST              :: Cached [ParseResult       l SimplifyAST]
    , fillUnnamedIds           :: Cached [ParseResult       l FillUnnamedIds]
    , enrichComments           :: Cached [ParseResult       l EnrichComments]
    , constructTranslationUnit :: Cached (C.TranslationUnit l ConstructTranslationUnit)
    , typecheckMacros          :: Cached (C.TranslationUnit l TypecheckMacros)
    , reparseMacroExpansions   :: Cached (C.TranslationUnit l ReparseMacroExpansions)
    , resolveBindingSpecs      :: Cached (C.TranslationUnit l ResolveBindingSpecs)
    , mangleNames              :: Cached (C.TranslationUnit l MangleNames)
    , adjustTypes              :: Cached (C.TranslationUnit l AdjustTypes)
    , translateTypes           :: Cached (C.TranslationUnit l TranslateTypes)
    , select                   :: Cached (C.TranslationUnit l Select)
    , final                    :: Cached (C.TranslationUnit l Final)
    }

{-------------------------------------------------------------------------------
  Traces
-------------------------------------------------------------------------------}

-- | Frontend trace messages
--
-- Most passes in the frontend have their own set of trace messages.
data FrontendMsg =
    FrontendClang                     ClangMsg
  | FrontendParse                    (Msg Parse)
  | FrontendSimplifyAST              (Msg SimplifyAST)
  | FrontendFillUnnamedIds           (Msg FillUnnamedIds)
  | FrontendPrepareReparse           (Msg PrepareReparse)
  | FrontendReparseMacroExpansions   (Msg ReparseMacroExpansions)
  | FrontendResolveBindingSpecs      (Msg ResolveBindingSpecs)
  | FrontendMangleNames              (Msg MangleNames)
  | FrontendTranslateTypes           (Msg TranslateTypes)
  | FrontendSelect                   (Msg Select)
  | FrontendCache                    (SafeTrace CacheMsg)
  | FrontendDoxygen                   DoxygenMsg
  | FrontendRootHeader                RootHeaderMsg
  deriving stock    (Show, Generic)
  deriving anyclass (PrettyForTrace, IsTrace Level)

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

-- | Information useful for inspection as well as peripheral tasks.
--
-- Excluded from the parse pass result because there is no 'Show' instance.
data ParseInfo = ParseInfo {
      includeGraph      :: IncludeGraph
    , getMainHeaders    :: GetMainHeaders
    }

{-------------------------------------------------------------------------------
  Internal helpers
-------------------------------------------------------------------------------}

data ParsePassResult l = ParsePassResult {
      results           :: [ParseResult l Parse]
    , doxygen           :: Doxygen
    , includeGraph      :: IncludeGraph
    , isMainHeader      :: IsMainHeader
    , isInMainHeaderDir :: IsInMainHeaderDir
    , getMainHeaders    :: GetMainHeaders
    , usageAnalysis     :: UnnamedIdUsageAnalysis
    , macroAnalysis     :: UniqueExpansion.Analysis
    }