packages feed

aihc-cabal-syntax-1.0.0.1: src/Aihc/Cabal/Internal/Condition.hs

{-# LANGUAGE OverloadedStrings #-}
-- | Parse a condition from the arguments of an @if@ or @elif@ section.
-- This module follows the condition parser of Cabal-syntax 3.12. The parser
-- reads section argument tokens, not text.
module Aihc.Cabal.Internal.Condition (parseCondition) where

import Control.Applicative (Alternative (..))
import Control.Monad (ap)
import Data.Char (isDigit, isAlphaNum)
import qualified Data.List.NonEmpty as NE
import Data.Text (Text)
import qualified Data.Text as T
import Text.Megaparsec (eof, runParser, satisfy, takeWhile1P)
import qualified Text.Megaparsec as M
import Text.Megaparsec.Char (char)
import Aihc.Cabal.Internal.Lexer (SectionArg (..))
import Aihc.Cabal.Internal.Types (Condition (..))
import Aihc.Cabal.Internal.Values (flagNameValue, identifier)
import Aihc.Cabal.Internal.Version

-- | A parser with Parsec semantics: an alternative runs only when the first
-- parser fails without input consumption.
newtype P a = P { runP :: [SectionArg] -> Reply a }

data Reply a = Ok a [SectionArg] Bool | Err Bool

instance Functor P where
  fmap f (P p) = P $ \s -> case p s of
    Ok a r c -> Ok (f a) r c
    Err c -> Err c

instance Applicative P where
  pure x = P (\s -> Ok x s False)
  (<*>) = ap

instance Monad P where
  P p >>= k = P $ \s -> case p s of
    Err c -> Err c
    Ok a r c -> case runP (k a) r of
      Ok b r' c' -> Ok b r' (c || c')
      Err c' -> Err (c || c')

instance Alternative P where
  empty = P (const (Err False))
  P p <|> P q = P $ \s -> case p s of
    Err False -> q s
    reply -> reply
  many p = some p <|> pure []
  some p = (:) <$> p <*> many p

instance MonadFail P where
  fail _ = empty

try :: P a -> P a
try (P p) = P $ \s -> case p s of
  Err _ -> Err False
  reply -> reply

tokenWith :: (SectionArg -> Maybe a) -> P a
tokenWith f = P $ \s -> case s of
  x : rest | Just a <- f x -> Ok a rest True
  _ -> Err False

name :: P Text
name = tokenWith $ \t -> case t of
  ArgName _ x -> Just x
  _ -> Nothing

word :: Text -> P ()
word w = tokenWith $ \t -> case t of
  ArgName _ x | x == w -> Just ()
  _ -> Nothing

oper :: Text -> P ()
oper o = tokenWith $ \t -> case t of
  ArgOther _ x | x == o -> Just ()
  _ -> Nothing

-- | Consume a name token and parse it completely. A parse error after the
-- token is an error with input consumption.
value :: Parser a -> P a
value p = do
  x <- name
  case runParser (p <* eof) "" x of
    Left _ -> P (const (Err True))
    Right a -> pure a

parens :: P a -> P a
parens p = oper "(" *> p <* oper ")"

sepBy1 :: P a -> P s -> P (NE.NonEmpty a)
sepBy1 p s = (NE.:|) <$> p <*> many (s *> p)

parseCondition :: [SectionArg] -> Maybe Condition
parseCondition args = case runP (condOr <* end) args of
  Ok c _ _ -> Just c
  Err _ -> Nothing
  where
    end = P $ \s -> if null s then Ok () s False else Err False
    condOr = foldl1 Or <$> sepBy1 condAnd (oper "||")
    condAnd = foldl1 And <$> sepBy1 cond (oper "&&")
    cond = boolean <|> parens condOr <|> (Not <$> (oper "!" *> cond))
      <|> (word "os" *> parens (OS <$> value identifier))
      <|> (word "arch" *> parens (Arch <$> value identifier))
      <|> (word "flag" *> parens (FlagValue <$> value flagNameValue))
      <|> (word "impl" *> parens (Impl <$> value compiler <*> (versionRange <|> pure anyVersion)))
    boolean = tokenWith $ \t -> case t of
      ArgName _ x
        | x `elem` ["True", "true"] -> Just (Literal True)
        | x `elem` ["False", "false"] -> Just (Literal False)
      _ -> Nothing
    compiler = do
      x <- takeWhile1P Nothing isAlphaNum
      if T.all isDigit x then fail "all digits compiler name" else pure x
    versionRange = expr
      where
        expr = foldl1 EitherRange <$> sepBy1 term (oper "||")
        term = foldl1 Both <$> sepBy1 factor (oper "&&")
        factor = parens expr
          <|> (AnyVersion <$ word "-any")
          <|> (noVersion <$ word "-none")
          <|> try (withinVersion <$> (oper "==" *> value versionStar) <* oper "*")
          <|> foldr1 (<|>) [try (f <$> (oper o *> value versionParser)) | (o, f) <- operators]
        operators =
          [ ("<", Earlier), ("<=", AtMost), (">", Later), (">=", AtLeast)
          , ("^>=", MajorBound), ("==", Equal) ]
        versionStar = specVersion <$> M.some (read <$> M.some (satisfy isDigit) <* char '.')