hs-bindgen-1.0.0.0: src-internal/HsBindgen/Frontend/Predicate.hs
-- | Select definitions from the C header
module HsBindgen.Frontend.Predicate (
-- * Booleans
Boolean (..)
, mergeBooleans
, eval
-- * Predicates
, HeaderPathPredicate (..)
, DeclPredicate (..)
, SelectionPredicate (..)
, Regex -- opaque
, matchTest
-- * Execution (internal API)
, IsMainHeader
, mkIsMainHeader
, IsInMainHeaderDir
, mkIsInMainHeaderDir
, matchSelect
) where
import Data.Function
import Data.List qualified as List
import Data.Set qualified as Set
import System.FilePath qualified as FilePath
import Text.Regex.PCRE qualified as PCRE
import Text.Regex.PCRE.Text ()
import Clang.Paths
import HsBindgen.Imports
import HsBindgen.IR.C qualified as C
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
-- | Boolean logic combining predicates that determine which declarations should
-- be kept
data Boolean a =
-- | Match any declaration
BTrue
-- | Match no declaration
| BFalse
-- | Logical conjunction
| BAnd (Boolean a) (Boolean a)
-- | Logical disjunction
| BOr (Boolean a) (Boolean a)
-- | Logical negation
| BNot (Boolean a)
-- | Concrete predicates
| BIf a
deriving stock (Show, Eq, Generic)
instance Default a => Default (Boolean a) where
def = BIf def
-- | Predicates matched against header paths
data HeaderPathPredicate =
-- | Only include declarations in main headers (not included headers)
FromMainHeaders
-- | Only include declarations in headers in main header directories,
-- including subdirectories
| FromMainHeaderDirs
-- | Include declarations in any header
--
-- Unlike 'BTrue', this excludes declarations without a header, such as
-- macros defined by root directives or @-D@ options.
| FromAllHeaders
-- | Match header path against regex
| HeaderPathMatches Regex
deriving stock (Show, Eq, Generic)
-- | Predicates matched against declarations themselves
data DeclPredicate =
-- | Match declaration name against regex
DeclNameMatches Regex
-- | Match deprecated declarations taking current target platform into
-- account; see 'HsBindgen.Frontend.AST.Decl.Availability'
| DeclDeprecated
deriving stock (Show, Eq, Generic)
-- | Predicates for the @Select@ pass
--
-- Selection predicates match against header file paths or the declarations
-- themselves.
--
-- The selection predicate dictates which declarations `hs-bindgen` generates
-- bindings for. For details, please see the @hs-bindgen@ manual section on
-- predicates and program slicing.
data SelectionPredicate =
SelectHeader HeaderPathPredicate
| SelectDecl DeclPredicate
deriving stock (Show, Eq, Generic)
instance Default SelectionPredicate where
def = SelectHeader FromMainHeaders
{-------------------------------------------------------------------------------
Execution
NOTE: This is internal API (users construct filters, but don't use them).
-------------------------------------------------------------------------------}
-- | Check if a declaration is from one of the main headers
--
-- Dealing with main headers is somewhat subtle. See
-- "HsBindgen.Frontend.ProcessIncludes" for discussion.
type IsMainHeader = RealPath -> Bool
-- | Construct an 'IsMainHeader' function for the given main header paths
mkIsMainHeader ::
Set RealPath -- ^ Main header paths
-> IsMainHeader
mkIsMainHeader paths path = path `Set.member` paths
-- | Check if a declaration is in a main header directory, including
-- subdirectories
type IsInMainHeaderDir = RealPath -> Bool
-- | Construct an 'IsInMainHeaderDir' function for the given main header paths
mkIsInMainHeaderDir ::
Set RealPath -- ^ Main header paths
-> IsInMainHeaderDir
mkIsInMainHeaderDir paths path =
let dir = FilePath.splitDirectories . FilePath.takeDirectory $
getRealPath path
in any (`List.isPrefixOf` dir) mainDirs
where
mainDirs :: [[FilePath]]
mainDirs = map FilePath.splitDirectories . Set.toList $
Set.map (FilePath.takeDirectory . getRealPath) paths
-- | Match 'SelectionPredicate' predicates
--
-- A declaration without a header (a root directive or a @-D@ option) matches
-- no 'HeaderPathPredicate'.
matchSelect ::
IsMainHeader
-> IsInMainHeaderDir
-> C.DeclPath
-> C.DeclName
-> C.Availability
-> Boolean SelectionPredicate
-> Bool
matchSelect isMainHeader isInMainHeaderDir path cDeclName availability = eval $ \case
SelectHeader p -> case path of
C.InHeader realPath -> matchHeaderPath isMainHeader isInMainHeaderDir realPath p
C.InRootHeader -> False
C.OnCommandLine -> False
SelectDecl p -> matchDecl cDeclName availability p
{-------------------------------------------------------------------------------
Merging
-------------------------------------------------------------------------------}
-- | Merge lists of negative and positive Booleans
--
-- Combine the negative Booleans using AND, and the positive Booleans using OR.
mergeBooleans :: forall a. Eq a => [Boolean a] -> [Boolean a] -> Boolean a
mergeBooleans negatives positives =
reduce $ BAnd neg pos
where
mergeNeg, mergePos :: Boolean a -> Boolean a -> Boolean a
mergeNeg p q = reduce $ BAnd (reduce $ BNot $ reduce p) q
mergePos p q = reduce $ BOr (reduce p) q
neg, pos :: Boolean a
neg = foldr mergeNeg BTrue negatives
pos = foldr mergePos BFalse positives
{-------------------------------------------------------------------------------
Internal auxiliary: execution
-------------------------------------------------------------------------------}
-- | Boolean logic reduction
--
-- * This is /not/ recursive: we call this at every step in 'eval'
-- * This needs to match the semantics of 'eval' precisely.
reduce :: Eq a => Boolean a -> Boolean a
reduce = \case
BNot (BNot p) -> p
BNot BTrue -> BFalse
BNot BFalse -> BTrue
--
BAnd BTrue q -> q
BAnd p BTrue -> p
BAnd p q | p == BFalse || q == BFalse -> BFalse
--
BOr BFalse q -> q
BOr p BFalse -> p
BOr p q | p == BTrue || q == BTrue -> BTrue
--
p -> p
-- | Evaluate a 'Boolean'
--
-- * This needs to match the semantics of 'reduce' precisely. It should be OK
-- as long as /obvious/ boolean interpretations are used.
eval :: forall a.
Eq a
=> (a -> Bool) -- ^ Evaluation function for concrete Booleans
-> Boolean a
-> Bool
eval f = go
where
go :: Boolean a -> Bool
go p = case reduce p of
BTrue -> True
BFalse -> False
BAnd p1 p2 -> go p1 && go p2
BOr p1 p2 -> go p1 || go p2
BNot p1 -> not (go p1)
BIf p1 -> f p1
-- | Match 'HeaderPathPredicate' predicates
matchHeaderPath ::
IsMainHeader
-> IsInMainHeaderDir
-> RealPath
-> HeaderPathPredicate
-> Bool
matchHeaderPath isMainHeader isInMainHeaderDir realPath = \case
FromMainHeaders -> isMainHeader realPath
FromMainHeaderDirs -> isInMainHeaderDir realPath
FromAllHeaders -> True
HeaderPathMatches re -> matchTest re (getRealPathText realPath)
-- | Match 'DeclPredicate' predicates
matchDecl :: C.DeclName -> C.Availability -> DeclPredicate -> Bool
matchDecl cDeclName availability = \case
DeclNameMatches re -> matchTest re $ C.renderDeclName cDeclName
DeclDeprecated -> isDeprecated
where
isDeprecated = case availability of
C.Deprecated -> True
_ -> False
{-------------------------------------------------------------------------------
Internal auxiliary: regexs
-------------------------------------------------------------------------------}
-- | Perl-compatible regular expression
data Regex = Regex{
string :: String
, compiled :: PCRE.Regex
}
instance Eq Regex where
(==) = (==) `on` (.string)
-- | Validity of the 'Show' instance depends on the 'IsString' instance
instance Show Regex where
show regex = show regex.string
instance IsString Regex where
fromString string = Regex{
string = string
, compiled = PCRE.makeRegex string
}
matchTest :: Regex -> Text -> Bool
matchTest regex = PCRE.matchTest regex.compiled