packages feed

th-printf-0.8: parser/Parser.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE NoMonomorphismRestriction #-}

module Parser where

import Control.Applicative hiding ((<|>))
import Control.Monad (when)
import Control.Monad.Fix
import Control.Monad.RWS
import Data.Char
import Data.CharSet hiding (map)
import Data.Maybe
import qualified Data.Set as S
import Lens.Micro.Platform
import Parser.Types
import Text.Parsec hiding (many)
import Text.ParserCombinators.ReadP (readP_to_S)
import Text.Read.Lex (lexChar)

type Warning = String

parseStr :: String -> Either ParseError ([Atom], [[Warning]])
parseStr = fmap (unzip . map normalizeAndWarn) . parse printfStr "" . lexChars
 where
  lexChars x = (`fix` x) $ \f s ->
    if Prelude.null s
      then []
      else case readP_to_S lexChar s of
        ((c, rest) : _) -> c : f rest
        [] -> error "malformed input"

normalizeAndWarn :: Atom -> (Atom, [Warning])
normalizeAndWarn s@Str{} = (s, [])
normalizeAndWarn (Arg f) = (Arg a, b)
 where
  (_, a, b) = runRWS (warnLength f >> go (spec f)) () f
  go c | c `elem` "aAeEfFgGxXo" = return ()
  go c | c `elem` "csqQ?" = warnSign >> warnPrefix >> warnZero >> warnSpace
  go c | c `elem` "diu" = warnPrefix
  go 'p' = warnSign >> warnPrefix >> warnZero
  go _ = undefined
  warnFlag ::
    (Eq a, MonadWriter [String] m, MonadState FormatArg m) =>
    Lens' FlagSet a ->
    a ->
    a ->
    Char ->
    m ()
  warnFlag lens' bad good flagName = do
    oldVal <- use (flags_ . lens')
    when (oldVal == bad) $ do
      c <- use spec_
      flags_ . lens' .= good
      tell
        [ "`"
            ++ [flagName]
            ++ "` flag has no effect on `"
            ++ [c]
            ++ "` specifier"
        ]
  warnSign = warnFlag signed_ True False '+'
  warnPrefix = warnFlag prefixed_ True False '#'
  warnSpace = warnFlag spaced_ True False ' '
  warnZero = warnFlag adjustment_ (Just ZeroPadded) Nothing '0'
  phonyLengthSpec =
    S.fromList $
      [(x, y) | x <- "diuoxX", y <- ["L"]]
        ++ [ (x, y)
           | x <- "fFeEgGaA"
           , y <- ["hh", "h", "l", "ll", "j", "z", "t"]
           ]
        ++ [(x, y) | x <- "csqQ", y <- ["hh", "h", "ll", "j", "z", "t", "L"]]
        ++ map ('p',) ["hh", "h", "l", "ll", "j", "z", "t", "L"]
  warnLength FormatArg{spec, lengthSpec = Just l}
    | (spec, show l) `S.member` phonyLengthSpec =
        tell
          [ "`"
              ++ show l
              ++ "` length modifier has no effect when combined with `"
              ++ [spec]
              ++ "` specifier"
          ]
  warnLength _ = return ()

flagSet :: CharSet
flagSet = fromList "-+ #0"

specSet :: CharSet
specSet = fromList "diuoxXfFeEaAgGpcsQq?"

lengthSpecifiers :: [(String, LengthSpecifier)]
lengthSpecifiers =
  [ ("hh", HH)
  , ("h", H)
  , ("ll", LL)
  , ("l", L)
  , ("j", J)
  , ("z", Z)
  , ("t", T)
  , ("L", BigL)
  ]

oneOfSet :: Stream s m Char => CharSet -> ParsecT s u m Char
oneOfSet s = satisfy (`member` s)

printfStr :: Stream s m Char => ParsecT s u m [Atom]
printfStr =
  many $
    Str "%" <$ try (string "%%")
      <|> Arg <$> fmtArg
      <|> Str <$> some (satisfy (/= '%'))

fmtArg :: Stream s m Char => ParsecT s u m FormatArg
fmtArg = do
  _ <- char '%'
  flags <- do
    fs <- many $ do
      c <- oneOfSet flagSet <?> "flag"
      pure $ case c of
        '-' -> FlagLJust
        '+' -> FlagSigned
        ' ' -> FlagSpaced
        '#' -> FlagPrefixed
        '0' -> FlagZeroPadded
        _ -> error "unreachable"
    let flagSet' = S.fromList fs
    if S.size flagSet' < length fs
      then fail "Duplicate flags specified"
      else pure $ toFlagSet flagSet'
  width <- numArg <?> "width"
  precision <- optionMaybe (char '.' *> numArg) <?> "precision"
  lengthSpec <-
    optionMaybe $ choice $ Prelude.map (\(a, b) -> b <$ string a) lengthSpecifiers
  spec <- oneOfSet specSet <?> "valid specifier"
  pure $ FormatArg flags width (fromMaybe (Given 0) <$> precision) spec lengthSpec
 where
  nat = do
    c <- many1 $ satisfy isDigit
    return (read c :: Integer)
  numArg = optionMaybe (Given <$> nat <|> Need <$ char '*')