249 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			249 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
| {-# LANGUAGE BangPatterns #-}
 | ||
| {-# LANGUAGE FlexibleContexts #-}
 | ||
| {-# LANGUAGE LambdaCase #-}
 | ||
| {-# LANGUAGE PackageImports #-}
 | ||
| {-# LANGUAGE ScopedTypeVariables #-}
 | ||
| {-# LANGUAGE StandaloneDeriving #-}
 | ||
| 
 | ||
| module Text.Megaparsec.Custom (
 | ||
|   -- * Custom parse error type
 | ||
|   CustomErr,
 | ||
| 
 | ||
|   -- * Throwing custom parse errors
 | ||
|   parseErrorAt,
 | ||
|   parseErrorAtRegion,
 | ||
|   withSource,
 | ||
| 
 | ||
|   -- * Pretty-printing custom parse errors
 | ||
|   customParseErrorPretty
 | ||
| )
 | ||
| where
 | ||
| 
 | ||
| import Prelude ()
 | ||
| import "base-compat-batteries" Prelude.Compat hiding (readFile)
 | ||
| 
 | ||
| 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
 | ||
| 
 | ||
| 
 | ||
| --- * Throwing custom parse errors
 | ||
| 
 | ||
| -- | Fail at a specific source position.
 | ||
| 
 | ||
| parseErrorAt :: MonadParsec CustomErr s m => SourcePos -> String -> m a
 | ||
| parseErrorAt pos msg = customFailure (ErrorFailAt pos (sourceColumn pos) msg)
 | ||
| {-# INLINABLE parseErrorAt #-}
 | ||
| 
 | ||
| -- | 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
 | ||
|   :: MonadParsec CustomErr s m
 | ||
|   => SourcePos -- ^ Start position
 | ||
|   -> SourcePos -- ^ End position
 | ||
|   -> String    -- ^ Error message
 | ||
|   -> m a
 | ||
| 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  customFailure (ErrorFailAt startPos endCol msg)
 | ||
| {-# INLINABLE parseErrorAtRegion #-}
 | ||
| 
 | ||
| -- | 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
 | ||
| 
 | ||
| 
 | ||
| --- * 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<br>
 | ||
| -- Copyright © 2007 Paolo Martini<br>
 | ||
| -- 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
 | ||
|         [] -> "<empty line>"
 | ||
|         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'
 | ||
| 
 |