{-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} module Text.Megaparsec.Custom ( -- * Custom parse error type CustomErr, -- * Constructing custom parse errors parseErrorAt, parseErrorAtRegion, withSource, -- * Pretty-printing custom parse errors customParseErrorPretty, -- * Final parse error types FinalParseError, FinalParseError', FinalParseErrorBundle, FinalParseErrorBundle', -- * Constructing final parse errors errorFinal, finalFancyFailure, finalFail, finalCustomFailure, -- * Handling errors from include files with final parse errors parseIncludeFile, attachSource, -- * Pretty-printing final parse errors finalParseErrorPretty, ) where import Prelude () import "base-compat-batteries" Prelude.Compat hiding (readFile) import Control.Monad.Except import Control.Monad.State.Strict (StateT, evalStateT) import Data.Foldable (asum, toList) import qualified Data.List.NonEmpty as NE import Data.Proxy (Proxy (Proxy)) import qualified Data.Set as S import Data.Text (Text) import Data.Void (Void) import Text.Megaparsec --- * Custom parse error type -- | A custom error type for the parser. The type is specialized to -- parsers of 'Text' streams. data CustomErr -- | Fail with a message at a specific source position interval. The -- interval must be contained within a single line. = ErrorFailAt SourcePos -- Starting position Pos -- Ending position (column; same line as start) String -- Error message -- | Attach a source file to a parse error (for error reporting from -- include files, e.g. with the 'region' parser combinator) | ErrorWithSource Text -- Source file contents (ParseError Char CustomErr) -- The original deriving (Show, Eq, Ord) -- We require an 'Ord' instance for 'CustomError' so that they may be -- stored in a 'Set'. The actual instance is inconsequential, so we just -- derive it, but this requires an (orphan) instance for 'ParseError'. -- Hopefully this does not cause any trouble. deriving instance (Ord c, Ord e) => Ord (ParseError c e) instance ShowErrorComponent CustomErr where showErrorComponent (ErrorFailAt _ _ errMsg) = errMsg showErrorComponent (ErrorWithSource _ e) = parseErrorTextPretty e --- * Constructing custom parse errors -- | Fail at a specific source position. parseErrorAt :: SourcePos -> String -> CustomErr parseErrorAt pos msg = ErrorFailAt pos (sourceColumn pos) msg -- | Fail at a specific source interval (within a single line). The -- interval is inclusive on the left and exclusive on the right; that is, -- it spans from the start position to just before (and not including) the -- end position. parseErrorAtRegion :: SourcePos -- ^ Start position -> SourcePos -- ^ End position -> String -- ^ Error message -> CustomErr parseErrorAtRegion startPos endPos msg = let startCol = sourceColumn startPos endCol' = mkPos $ subtract 1 $ unPos $ sourceColumn endPos endCol = if startCol <= endCol' && sourceLine startPos == sourceLine endPos then endCol' else startCol in ErrorFailAt startPos endCol msg -- | Attach a source file to a parse error. Intended for use with the -- 'region' parser combinator. withSource :: Text -> ParseError Char CustomErr -> ParseError Char CustomErr withSource s e = FancyError (errorPos e) $ S.singleton $ ErrorCustom $ ErrorWithSource s e --- * Pretty-printing custom parse errors -- | Pretty-print our custom parse errors and display the line on which -- the parse error occured. Use this instead of 'parseErrorPretty'. -- -- If any custom errors are present, arbitrarily take the first one (since -- only one custom error should be used at a time). customParseErrorPretty :: Text -> ParseError Char CustomErr -> String customParseErrorPretty source err = case findCustomError err of Nothing -> customParseErrorPretty' source err pos1 Just (ErrorWithSource customSource customErr) -> customParseErrorPretty customSource customErr Just (ErrorFailAt sourcePos col errMsg) -> let newPositionStack = sourcePos NE.:| NE.tail (errorPos err) errorIntervalLength = mkPos $ max 1 $ unPos col - unPos (sourceColumn sourcePos) + 1 newErr :: ParseError Char Void newErr = FancyError newPositionStack (S.singleton (ErrorFail errMsg)) in customParseErrorPretty' source newErr errorIntervalLength where findCustomError :: ParseError Char CustomErr -> Maybe CustomErr findCustomError err = case err of FancyError _ errSet -> finds (\case {ErrorCustom e -> Just e; _ -> Nothing}) errSet _ -> Nothing finds :: (Foldable t) => (a -> Maybe b) -> t a -> Maybe b finds f = asum . map f . toList --- * Final parse error types -- | A parse error type intended for throwing parse errors without the -- possiblity of backtracking. Intended for use as the error type in an -- 'ExceptT' layer of the parser. -- -- In order to pretty-print a parse error, we must bundle it with the -- source text and its filepaths (the 'ErrorBundle' constructor). However, -- when an error is thrown from within a parser, we do not have access to -- the (full) source, so we must hold the parse error until it can be -- joined with the source text and its filepath by the parser's caller -- (the 'ErrorFinal' constructor). data FinalParseError' e = ErrorFinal (ParseError Char e) | ErrorBundle (FinalParseErrorBundle' e) deriving (Show) type FinalParseError = FinalParseError' CustomErr -- A 'Monoid' instance is necessary for 'ExceptT (FinalParseError'' e)' to -- be an instance of Alternative and MonadPlus, which are required for the -- use of e.g. the 'many' parser combinator. This monoid instance simply -- takes the first (left-most) error. instance Semigroup (FinalParseError' e) where e <> _ = e instance Monoid (FinalParseError' e) where mempty = ErrorFinal $ FancyError (initialPos "" NE.:| []) (S.singleton (ErrorFail "default parse error")) mappend = (<>) -- | A type bundling a 'ParseError' with its source file and a stack of -- include file paths (for pretty printing). Although Megaparsec 6 -- maintains a stack of source files, making a field of this type -- redundant, this capability will be removed in Megaparsec 7. Therefore, -- we implement stacks of source files here for a smoother transition in -- the future. data FinalParseErrorBundle' e = FinalParseErrorBundle' { finalParseError :: ParseError Char e , errorSource :: Text , sourceFileStack :: NE.NonEmpty FilePath } deriving (Show) type FinalParseErrorBundle = FinalParseErrorBundle' CustomErr --- * Constructing and throwing final parse errors -- | Convert a "regular" parse error into a "final" parse error. errorFinal :: ParseError Char e -> FinalParseError' e errorFinal = ErrorFinal -- | Like 'fancyFailure', but as a "final" parse error. finalFancyFailure :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => S.Set (ErrorFancy e) -> m a finalFancyFailure errSet = do pos <- getPosition let parseErr = FancyError (pos NE.:| []) errSet throwError $ ErrorFinal parseErr -- | Like 'fail', but as a "final" parse error. finalFail :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => String -> m a finalFail = finalFancyFailure . S.singleton . ErrorFail -- | Like 'customFailure', but as a "final" parse error. finalCustomFailure :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => e -> m a finalCustomFailure = finalFancyFailure . S.singleton . ErrorCustom --- * Handling errors from include files with "final" parse errors -- Some care must be taken for sources to be attached to the right parse -- errors when dealing with include files, so we capture the necessary -- procedure in this function. parseIncludeFile :: forall st m a. Monad m => StateT st (ParsecT CustomErr Text (ExceptT FinalParseError m)) a -> st -> FilePath -> Text -> StateT st (ParsecT CustomErr Text (ExceptT FinalParseError m)) a parseIncludeFile parser initState filepath text = catchError parser' handler where parser' = do eResult <- lift $ lift $ runParserT (evalStateT parser initState) filepath text case eResult of Left parseError -> throwError $ errorFinal parseError Right result -> pure result handler e = throwError $ ErrorBundle $ attachSource filepath text e attachSource :: FilePath -> Text -> FinalParseError' e -> FinalParseErrorBundle' e attachSource filePath sourceText finalParseError = case finalParseError of ErrorFinal parseError -> FinalParseErrorBundle' { finalParseError = parseError , errorSource = sourceText , sourceFileStack = filePath NE.:| [] } ErrorBundle bundle -> bundle { sourceFileStack = filePath NE.<| sourceFileStack bundle } --- * Pretty-printing final parse errors -- | Pretty-print a "final" parse error: print the stack of include files, -- then apply the pretty-printer for custom parse errors. finalParseErrorPretty :: FinalParseErrorBundle' CustomErr -> String finalParseErrorPretty bundle = concatMap printIncludeFile (NE.init (sourceFileStack bundle)) <> customParseErrorPretty (errorSource bundle) (finalParseError bundle) where printIncludeFile path = "in file included from " <> path <> ",\n" --- * Modified Megaparsec source -- The below code has been copied from Megaparsec (v.6.4.1, -- Text.Megaparsec.Error) and modified to suit our needs. These changes are -- indicated by square brackets. The following copyright notice, conditions, -- and disclaimer apply to all code below this point. -- -- Copyright © 2015–2018 Megaparsec contributors
-- Copyright © 2007 Paolo Martini
-- Copyright © 1999–2000 Daan Leijen -- -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without -- modification, are permitted provided that the following conditions are met: -- -- * Redistributions of source code must retain the above copyright notice, -- this list of conditions and the following disclaimer. -- -- * Redistributions in binary form must reproduce the above copyright notice, -- this list of conditions and the following disclaimer in the documentation -- and/or other materials provided with the distribution. -- -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS -- OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -- OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN -- NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, -- INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -- OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -- EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -- | Pretty-print a 'ParseError Char CustomErr' and display the line on -- which the parse error occurred. The rendered 'String' always ends with -- a newline. customParseErrorPretty' :: ( ShowToken (Token s) , LineToken (Token s) , ShowErrorComponent e , Stream s ) => s -- ^ Original input stream -> ParseError (Token s) e -- ^ Parse error to render -> Pos -- ^ Length of error interval [added] -> String -- ^ Result of rendering customParseErrorPretty' = customParseErrorPretty_ defaultTabWidth customParseErrorPretty_ :: forall s e. ( ShowToken (Token s) , LineToken (Token s) , ShowErrorComponent e , Stream s ) => Pos -- ^ Tab width -> s -- ^ Original input stream -> ParseError (Token s) e -- ^ Parse error to render -> Pos -- ^ Length of error interval [added] -> String -- ^ Result of rendering customParseErrorPretty_ w s e l = sourcePosStackPretty (errorPos e) <> ":\n" <> padding <> "|\n" <> lineNumber <> " | " <> rline <> "\n" <> padding <> "| " <> rpadding <> highlight <> "\n" <> -- [added `highlight`] parseErrorTextPretty e where epos = NE.head (errorPos e) -- [changed from NE.last to NE.head] lineNumber = (show . unPos . sourceLine) epos padding = replicate (length lineNumber + 1) ' ' rpadding = replicate (unPos (sourceColumn epos) - 1) ' ' highlight = replicate (unPos l) '^' -- [added] rline = case rline' of [] -> "" xs -> expandTab w xs rline' = fmap tokenAsChar . chunkToTokens (Proxy :: Proxy s) $ selectLine (sourceLine epos) s -- | Select a line from input stream given its number. selectLine :: forall s. (LineToken (Token s), Stream s) => Pos -- ^ Number of line to select -> s -- ^ Input stream -> Tokens s -- ^ Selected line selectLine l = go pos1 where go !n !s = if n == l then fst (takeWhile_ notNewline s) else go (n <> pos1) (stripNewline $ snd (takeWhile_ notNewline s)) notNewline = not . tokenIsNewline stripNewline s = case take1_ s of Nothing -> s Just (_, s') -> s' -- | Replace tab characters with given number of spaces. expandTab :: Pos -> String -> String expandTab w' = go 0 where go 0 [] = [] go 0 ('\t':xs) = go w xs go 0 (x:xs) = x : go 0 xs go !n xs = ' ' : go (n - 1) xs w = unPos w'