packages feed

tilia-0.1.0.0: src/Tilia/Cpp/Macros.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Inferences regarding CPP macros that we can make based on the build
-- plan and on the answers a configuration gives.
module Tilia.Cpp.Macros
  ( Macros (..),
    guardHolds,
    impliedBy,
  )
where

import Control.Applicative ((<|>))
import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
import Data.List (dropWhileEnd)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T

-- | What is known about macros.
data Macros = Macros
  { -- | The macros that take a version apart and compare it:
    -- @MIN_VERSION_containers@ and the like, each under the version the
    -- plan resolved that package to. @MIN_VERSION_GLASGOW_HASKELL@ is one
    -- of these, under the compiler's four-part version.
    macroVersions :: Map Text [Integer],
    -- | The macros that stand for a number, which is @__GLASGOW_HASKELL__@
    -- and its patch levels.
    macroNumbers :: Map Text Integer,
    -- | The macros known to be defined, though not to what.
    macroDefined :: Set Text,
    -- | The macros known not to be defined, such as those of the compilers
    -- other than the one the plan is for, like @__MHS__@.
    macroUndefined :: Set Text,
    -- | The version, or number, each macro is known to reach, a number
    -- being a version of one part.
    macroAtLeast :: Map Text [Integer],
    -- | The version, or number, each macro is known to stay below.
    macroBelow :: Map Text [Integer]
  }
  deriving (Eq, Show)

instance Semigroup Macros where
  a <> b =
    Macros
      { macroVersions = macroVersions a <> macroVersions b,
        macroNumbers = macroNumbers a <> macroNumbers b,
        macroDefined = macroDefined a <> macroDefined b,
        macroUndefined = macroUndefined a <> macroUndefined b,
        macroAtLeast = Map.unionWith max (macroAtLeast a) (macroAtLeast b),
        macroBelow = Map.unionWith min (macroBelow a) (macroBelow b)
      }

instance Monoid Macros where
  mempty =
    Macros
      { macroVersions = Map.empty,
        macroNumbers = Map.empty,
        macroDefined = Set.empty,
        macroUndefined = Set.empty,
        macroAtLeast = Map.empty,
        macroBelow = Map.empty
      }

-- | Does a conditional's guard hold, where what is known settles it?
-- 'Nothing' is no answer rather than a negative one.
guardHolds :: Macros -> Text -> Maybe Bool
guardHolds macros written = (/= 0) <$> (valueOf macros =<< expressionOf written)

-- | What a guard holding, or failing, implies about the macros.
impliedBy :: Bool -> Text -> Macros
impliedBy holds = maybe mempty (go holds) . expressionOf
  where
    go h = \case
      Not e -> go (not h) e
      And a b | h -> go h a <> go h b
      Or a b | not h -> go h a <> go h b
      Defined n
        | h -> mempty{macroDefined = Set.singleton n}
        | otherwise -> mempty{macroUndefined = Set.singleton n}
      Reaches n v
        | h -> mempty{macroAtLeast = Map.singleton n (trimmed v)}
        | otherwise -> mempty{macroBelow = Map.singleton n (trimmed v)}
      Macro n | h -> mempty{macroDefined = Set.singleton n}
      _ -> mempty

-- | Whether a macro is defined, where that is known.
isDefined :: Macros -> Text -> Maybe Bool
isDefined macros n
  | Map.member n (macroVersions macros)
      || Map.member n (macroNumbers macros)
      || Set.member n (macroDefined macros) =
      Just True
  | Set.member n (macroUndefined macros) = Just False
  | otherwise = Nothing

-- | The number a macro stands for, where that is known.
numberOf :: Macros -> Text -> Maybe Integer
numberOf macros n = case isDefined macros n of
  Just False -> Just 0
  _ -> Map.lookup n (macroNumbers macros)

-- | Does a macro's version, or number, reach this one, where what is known
-- settles it?
reaches :: Macros -> Text -> [Integer] -> Maybe Bool
reaches macros n wanted = case held of
  Just v -> Just (trimmed v >= w)
  Nothing
    | any (>= w) (Map.lookup n (macroAtLeast macros)) -> Just True
    | any (<= w) (Map.lookup n (macroBelow macros)) -> Just False
    | otherwise -> Nothing
  where
    w = trimmed wanted
    held =
      Map.lookup n (macroVersions macros) <|> (pure <$> numberOf macros n)

-- | A version without the zeros at its end, which 'compare' orders as
-- versions are ordered.
trimmed :: [Integer] -> [Integer]
trimmed = dropWhileEnd (== 0)

-- | The expression of a guard, where we read it.
expressionOf :: Text -> Maybe Expr
expressionOf written = case T.span isNameChar (T.stripStart written) of
  (keyword, rest)
    | keyword `elem` ["if", "elif"] -> case orExpr =<< tokensOf rest of
        Just (e, []) -> Just e
        _ -> Nothing
    | keyword `elem` ["ifdef", "elifdef"] -> Defined <$> nameOf rest
    | keyword `elem` ["ifndef", "elifndef"] -> Not . Defined <$> nameOf rest
    | otherwise -> Nothing
  where
    nameOf rest = case tokensOf rest of
      Just [Name n] -> Just n
      _ -> Nothing

-- | The expression of a guard.
data Expr
  = Or Expr Expr
  | And Expr Expr
  | Not Expr
  | -- | Two expressions and the test comparing them.
    Compared (Integer -> Integer -> Bool) Expr Expr
  | -- | Whether a macro is defined.
    Defined Text
  | -- | Whether a macro's version, or number, reaches this one.
    Reaches Text [Integer]
  | -- | The number a macro stands for.
    Macro Text
  | Literal Integer
  | -- | A call of a macro other than a version test.
    Call

-- | What an expression evaluates to, if anything.
valueOf :: Macros -> Expr -> Maybe Integer
valueOf macros = \case
  Or a b
    | any true [value a, value b] -> Just 1
    | all false [value a, value b] -> Just 0
    | otherwise -> Nothing
  And a b
    | any false [value a, value b] -> Just 0
    | all true [value a, value b] -> Just 1
    | otherwise -> Nothing
  Not a -> fromBool . (== 0) <$> value a
  Compared test a b -> fromBool <$> (test <$> value a <*> value b)
  Defined n -> fromBool <$> isDefined macros n
  Reaches n v -> fromBool <$> reaches macros n v
  Macro n -> numberOf macros n
  Literal n -> Just n
  Call -> Nothing
  where
    value = valueOf macros
    true = maybe False (/= 0)
    false = maybe False (== 0)
    fromBool b = if b then 1 else 0

-- | One piece of a guard.
data Token
  = Name Text
  | Number Integer
  | Punct Text
  deriving (Eq, Show)

-- | Take a guard apart, or refuse it whole.
tokensOf :: Text -> Maybe [Token]
tokensOf = go . T.stripStart
  where
    go t
      | T.null t = Just []
      | Just (c, _) <- T.uncons t,
        isNameStart c =
          let (n, rest) = T.span isNameChar t in (Name n :) <$> next rest
      | Just (c, _) <- T.uncons t,
        isDigit c =
          let (digits, rest) = T.span isDigit t
              rest' = T.dropWhile (`T.elem` "uUlL") rest
           in case T.uncons rest' of
                Just (c', _) | isNameChar c' || c' == '.' -> Nothing
                _ -> (Number (readDigits digits) :) <$> next rest'
      | Just punct <- firstThat (`T.stripPrefix` t) punctuation =
          (Punct (T.take (T.length t - T.length punct) t) :) <$> next punct
      | otherwise = Nothing
    next = go . T.stripStart
    firstThat f = foldr (\x acc -> maybe acc Just (f x)) Nothing
    readDigits = T.foldl' (\n c -> n * 10 + toInteger (fromEnum c - fromEnum '0')) 0

-- | An expression, and what is left of the tokens after it.
type Reading = Maybe (Expr, [Token])

orExpr :: [Token] -> Reading
orExpr ts = do
  (left, rest) <- andExpr ts
  more left rest
  where
    more left = \case
      Punct "||" : rest -> do
        (right, rest') <- andExpr rest
        more (Or left right) rest'
      rest -> Just (left, rest)

andExpr :: [Token] -> Reading
andExpr ts = do
  (left, rest) <- compared ts
  more left rest
  where
    more left = \case
      Punct "&&" : rest -> do
        (right, rest') <- compared rest
        more (And left right) rest'
      rest -> Just (left, rest)

compared :: [Token] -> Reading
compared ts = do
  (left, rest) <- unary ts
  case rest of
    Punct op : rest' | Just test <- comparison op -> do
      (right, rest'') <- unary rest'
      let e = fromMaybe (Compared test left right) (reaching op left right)
      pure (e, rest'')
    _ -> Just (left, rest)
  where
    comparison = \case
      "==" -> Just (==)
      "!=" -> Just (/=)
      "<" -> Just (<)
      ">" -> Just (>)
      "<=" -> Just (<=)
      ">=" -> Just (>=)
      _ -> Nothing

-- | A macro compared with a number, as what it asks of the macro's number.
reaching :: Text -> Expr -> Expr -> Maybe Expr
reaching op (Macro n) (Literal m) = case op of
  ">=" -> Just (Reaches n [m])
  ">" -> Just (Reaches n [m + 1])
  "<" -> Just (Not (Reaches n [m]))
  "<=" -> Just (Not (Reaches n [m + 1]))
  "==" -> Just exactly
  "!=" -> Just (Not exactly)
  _ -> Nothing
  where
    exactly = And (Reaches n [m]) (Not (Reaches n [m + 1]))
reaching _ _ _ = Nothing

unary :: [Token] -> Reading
unary = \case
  Punct "!" : rest -> do
    (e, rest') <- unary rest
    pure (Not e, rest')
  Punct "(" : rest -> do
    (e, rest') <- orExpr rest
    case rest' of
      Punct ")" : rest'' -> Just (e, rest'')
      _ -> Nothing
  Name "defined" : rest -> case rest of
    Name n : rest' -> Just (Defined n, rest')
    Punct "(" : Name n : Punct ")" : rest' -> Just (Defined n, rest')
    _ -> Nothing
  Name n : Punct "(" : rest -> do
    (arguments, rest') <- argumentList rest
    pure $ case arguments of
      Just v | "MIN_VERSION_" `T.isPrefixOf` n -> (Reaches n v, rest')
      _ -> (Call, rest')
  Name n : rest -> Just (Macro n, rest)
  Number n : rest -> Just (Literal n, rest)
  _ -> Nothing

argumentList :: [Token] -> Maybe (Maybe [Integer], [Token])
argumentList ts = do
  (inside, rest) <- upToClose (0 :: Int) [] ts
  pure (numbersOf inside, rest)
  where
    upToClose depth acc = \case
      Punct ")" : rest
        | depth == 0 -> Just (reverse acc, rest)
        | otherwise -> upToClose (depth - 1) (Punct ")" : acc) rest
      Punct "(" : rest -> upToClose (depth + 1) (Punct "(" : acc) rest
      t : rest -> upToClose depth (t : acc) rest
      [] -> Nothing
    numbersOf = \case
      [Number n] -> Just [n]
      Number n : Punct "," : rest -> (n :) <$> numbersOf rest
      _ -> Nothing

-- | The punctuation of the expressions we read, longest first so that @<=@
-- is never taken for @<@.
punctuation :: [Text]
punctuation = ["&&", "||", "==", "!=", "<=", ">=", "(", ")", ",", "!", "<", ">"]

isNameStart :: Char -> Bool
isNameStart c = isAsciiLower c || isAsciiUpper c || c == '_'

isNameChar :: Char -> Bool
isNameChar c = isNameStart c || isDigit c