packages feed

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

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

-- | The questions a module's conditionals ask that the build plan has
-- already answered.
module Tilia.Cpp.Macros
  ( Macros (..),
    answerTo,
  )
where

import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T

-- | What the preprocessor would have been told.
data Macros = Macros
  { -- | The macros that take a version apart and compare it:
    -- @MIN_VERSION_containers@ and its 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
  }
  deriving (Eq, Show)

-- | What a conditional's guard comes to, where what is known settles it.
--
-- The text is the whole directive as it was written after its hash, keyword
-- and all, because @#if@ and @#ifdef@ ask about their rest in different
-- ways.
answerTo :: Macros -> Text -> Maybe Bool
answerTo macros written = case T.span isNameChar (T.stripStart written) of
  (keyword, rest) -> case keyword of
    "if" -> (/= 0) <$> evaluate macros rest
    "elif" -> (/= 0) <$> evaluate macros rest
    "ifdef" -> nameIsKnown rest
    "elifdef" -> nameIsKnown rest
    "ifndef" -> not <$> nameIsKnown rest
    "elifndef" -> not <$> nameIsKnown rest
    _ -> Nothing
  where
    nameIsKnown rest = case tokensOf rest of
      Just [Name n] | known macros n -> Just True
      _ -> Nothing

-- | Is this a macro whose value we know?
known :: Macros -> Text -> Bool
known macros n =
  Map.member n (macroVersions macros) || Map.member n (macroNumbers macros)

----------------------------------------------------------------------------
-- The expression

-- | What an expression came to, where it came to anything.
evaluate :: Macros -> Text -> Maybe Integer
evaluate macros written = case tokensOf written of
  Nothing -> Nothing
  Just ts -> case orExpr macros ts of
    Just (value, []) -> value
    _ -> Nothing

-- | An expression, and what is left of the tokens after it.
--
-- The outer 'Maybe' is whether it could be read at all; the inner one is
-- whether what it means is known.
type Reading = Maybe (Maybe Integer, [Token])

orExpr :: Macros -> [Token] -> Reading
orExpr macros ts = do
  (left, rest) <- andExpr macros ts
  more left rest
  where
    more left = \case
      Punct "||" : rest -> do
        (right, rest') <- andExpr macros rest
        more (either' left right) rest'
      rest -> Just (left, rest)
    either' a b
      | any true [a, b] = Just 1
      | all false [a, b] = Just 0
      | otherwise = Nothing

andExpr :: Macros -> [Token] -> Reading
andExpr macros ts = do
  (left, rest) <- compared macros ts
  more left rest
  where
    more left = \case
      Punct "&&" : rest -> do
        (right, rest') <- compared macros rest
        more (both left right) rest'
      rest -> Just (left, rest)
    both a b
      | any false [a, b] = Just 0
      | all true [a, b] = Just 1
      | otherwise = Nothing

true, false :: Maybe Integer -> Bool
true = maybe False (/= 0)
false = maybe False (== 0)

compared :: Macros -> [Token] -> Reading
compared macros ts = do
  (left, rest) <- unary macros ts
  case rest of
    Punct op : rest' | Just test <- comparison op -> do
      (right, rest'') <- unary macros rest'
      pure (fromBool . uncurry test <$> pair left right, rest'')
    _ -> Just (left, rest)
  where
    pair a b = (,) <$> a <*> b
    fromBool b = if b then 1 else 0
    comparison = \case
      "==" -> Just (==)
      "!=" -> Just (/=)
      "<" -> Just (<)
      ">" -> Just (>)
      "<=" -> Just (<=)
      ">=" -> Just (>=)
      _ -> Nothing

unary :: Macros -> [Token] -> Reading
unary macros = \case
  Punct "!" : rest -> do
    (value, rest') <- unary macros rest
    pure (negated <$> value, rest')
  Punct "(" : rest -> do
    (value, rest') <- orExpr macros rest
    case rest' of
      Punct ")" : rest'' -> Just (value, rest'')
      _ -> Nothing
  Name "defined" : rest -> case rest of
    Name n : rest' -> Just (asKnown n, rest')
    Punct "(" : Name n : Punct ")" : rest' -> Just (asKnown n, rest')
    _ -> Nothing
  Name n : Punct "(" : rest -> do
    (arguments, rest') <- argumentList rest
    pure (atLeast <$> Map.lookup n (macroVersions macros) <*> arguments, rest')
  Name n : rest -> Just (Map.lookup n (macroNumbers macros), rest)
  Number n : rest -> Just (Just n, rest)
  _ -> Nothing
  where
    negated n = if n == 0 then 1 else 0
    asKnown n = if known macros n then Just 1 else Nothing
    atLeast held wanted = if pad held >= pad wanted then 1 else 0
      where
        width = max (length held) (length wanted)
        pad v = take width (v <> repeat 0)

-- | What an application was given, and what follows its closing bracket.
--
-- Every argument has to be a plain number. One written as an expression is
-- not something to work out—a module that writes one is not asking the
-- question this can answer—but its brackets are still counted through, so
-- that the rest of the guard can be read and go on deciding what it can.
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 tokens

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

-- | Take a guard apart, or refuse it whole.
--
-- Refusing is not a failure. A guard with arithmetic in it, or a character
-- literal, or a hexadecimal constant, is one this does not read, and a
-- guard it does not read is a question left open.
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

-- | 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