-- | -- Module : Text.Megaparsec.Error -- Copyright : © 2015–2016 Megaparsec contributors -- © 2007 Paolo Martini -- © 1999–2001 Daan Leijen -- License : FreeBSD -- -- Maintainer : Mark Karpov -- Stability : experimental -- Portability : portable -- -- Parse errors. module Text.Megaparsec.Error ( Message (..) , isUnexpected , isExpected , isMessage , messageString , badMessage , ParseError , errorPos , errorMessages , errorIsUnknown , newErrorMessage , newErrorMessages , newErrorUnknown , addErrorMessage , addErrorMessages , setErrorMessage , setErrorPos , mergeError , showMessages ) where import Control.Exception (Exception) import Data.Foldable (find, concat) import Data.List (intercalate) import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (fromMaybe, fromJust) import Data.Semigroup (Semigroup((<>))) import Data.Typeable (Typeable) import Prelude hiding (concat) import qualified Data.List.NonEmpty as NE import Text.Megaparsec.Pos #if !MIN_VERSION_base(4,8,0) import Control.Applicative ((<$>)) import Data.Foldable (foldMap) import Data.Monoid (Monoid(..)) #endif -- | This data type represents parse error messages. data Message = Unexpected !String -- ^ Parser ran into an unexpected token | Expected !String -- ^ What is expected instead | Message !String -- ^ General-purpose error message component deriving (Show, Eq, Ord) -- | Check if given 'Message' is created with 'Unexpected' constructor. -- -- @since 4.4.0 isUnexpected :: Message -> Bool isUnexpected (Unexpected _) = True isUnexpected _ = False -- | Check if given 'Message' is created with 'Expected' constructor. -- -- @since 4.4.0 isExpected :: Message -> Bool isExpected (Expected _) = True isExpected _ = False -- | Check if given 'Message' is created with 'Message' constructor. -- -- @since 4.4.0 isMessage :: Message -> Bool isMessage (Message _) = True isMessage _ = False -- | Extract the message string from an error message. messageString :: Message -> String messageString (Unexpected s) = s messageString (Expected s) = s messageString (Message s) = s -- | Test if message string is empty. badMessage :: Message -> Bool badMessage = null . messageString -- | The data type @ParseError@ represents parse errors. It provides the -- source position ('SourcePos') of the error and a list of error messages -- ('Message'). data ParseError = ParseError { -- | Extract the source position from 'ParseError'. errorPos :: !SourcePos -- | Extract the list of error messages from 'ParseError'. , errorMessages :: [Message] } deriving (Eq, Typeable) instance Show ParseError where show e = show (errorPos e) ++ ":\n" ++ showMessages (errorMessages e) instance Monoid ParseError where mempty = newErrorUnknown (initialPos "") mappend = (<>) instance Semigroup ParseError where (<>) = mergeError instance Exception ParseError -- | Test whether given 'ParseError' has associated collection of error -- messages. Return @True@ if it has none and @False@ otherwise. errorIsUnknown :: ParseError -> Bool errorIsUnknown (ParseError _ ms) = null ms -- | @newErrorMessage m pos@ creates 'ParseError' with message @m@ and -- associated position @pos@. If message @m@ has empty message string, it -- won't be included. newErrorMessage :: Message -> SourcePos -> ParseError newErrorMessage m = newErrorMessages [m] -- | @newErrorMessages ms pos@ creates 'ParseError' with messages @ms@ and -- associated position @pos@. -- -- @since 4.2.0 newErrorMessages :: [Message] -> SourcePos -> ParseError newErrorMessages ms pos = addErrorMessages ms $ newErrorUnknown pos -- | @newErrorUnknown pos@ creates 'ParseError' without any associated -- message but with specified position @pos@. newErrorUnknown :: SourcePos -> ParseError newErrorUnknown pos = ParseError pos [] -- | @addErrorMessage m err@ returns @err@ with message @m@ added. This -- function makes sure that list of messages is always sorted and doesn't -- contain duplicates or messages with empty message strings. addErrorMessage :: Message -> ParseError -> ParseError addErrorMessage m (ParseError pos ms) = ParseError pos $ if badMessage m then ms else pre ++ [m] ++ post where pre = filter (< m) ms post = filter (> m) ms -- | @addErrorMessages ms err@ returns @err@ with messages @ms@ added. The -- function is defined in terms of 'addErrorMessage'. -- -- @since 4.2.0 addErrorMessages :: [Message] -> ParseError -> ParseError addErrorMessages ms err = foldr addErrorMessage err ms -- | @setErrorMessage m err@ returns @err@ with message @m@ added. This -- function also deletes all existing error messages that were created with -- the same constructor as @m@. If message @m@ has empty message string, the -- function does not add the message to the result (it still deletes all -- messages of the same type, though). setErrorMessage :: Message -> ParseError -> ParseError setErrorMessage m (ParseError pos ms) = if badMessage m then err else addErrorMessage m err where err = ParseError pos (filter (not . f) ms) f = fromJust $ find ($ m) [isUnexpected, isExpected, isMessage] -- | @setErrorPos pos err@ returns 'ParseError' identical to @err@, but with -- position @pos@. setErrorPos :: SourcePos -> ParseError -> ParseError setErrorPos pos (ParseError _ ms) = ParseError pos ms -- | Merge two error data structures into one joining their collections of -- messages and preferring longest match. In other words, earlier error -- message is discarded. This may seem counter-intuitive, but @mergeError@ -- is only used to merge error messages of alternative branches of parsing -- and in this case longest match should be preferred. mergeError :: ParseError -> ParseError -> ParseError mergeError e1@(ParseError pos1 _) e2@(ParseError pos2 ms2) = case pos1 `compare` pos2 of LT -> e2 EQ -> addErrorMessages ms2 e1 GT -> e1 {-# INLINE mergeError #-} -- | @showMessages ms@ transforms list of error messages @ms@ into -- their textual representation. showMessages :: [Message] -> String showMessages [] = "unknown parse error" showMessages ms = tail $ foldMap (fromMaybe "") (zipWith f ns rs) where (unexpected, ms') = span isUnexpected ms (expected, messages) = span isExpected ms' f prefix m = (prefix ++) <$> m ns = ["\nunexpected ","\nexpecting ","\n"] rs = (renderMsgs orList <$> [unexpected, expected]) ++ [renderMsgs (concat . NE.intersperse "\n") messages] -- | Render collection of messages. If the collection is empty, return -- 'Nothing', otherwise return textual representation of the messages inside -- 'Just'. renderMsgs :: (NonEmpty String -> String) -- ^ Function to combine results -> [Message] -- ^ Collection of messages to render -> Maybe String -- ^ Result, if any -- renderMsgs _ [] = Nothing renderMsgs f ms = f . fmap messageString <$> NE.nonEmpty ms -- | Print a pretty list where items are separated with commas and the word -- “or” according to rules of English punctuation. orList :: NonEmpty String -> String orList (x:|[]) = x orList (x:|[y]) = x ++ " or " ++ y orList xs = intercalate ", " (NE.init xs) ++ ", or " ++ NE.last xs