packages feed

gigaparsec-0.3.1.0: src/Text/Gigaparsec/Errors/ErrorGen.hs

{-# LANGUAGE Safe #-}
{-# LANGUAGE OverloadedLists, RecordWildCards #-}
{-# OPTIONS_GHC -Wno-partial-fields #-}
{-|
Module      : Text.Gigaparsec.Expr
Description : This module can be used to generate hand-tuned error messages without using monadic bind.
License     : BSD-3-Clause
Maintainer  : Jamie Willis, Gigaparsec Maintainers
Stability   : experimental

This module can be used to generate hand-tuned error messages without using monadic bind.

-}
module Text.Gigaparsec.Errors.ErrorGen (
    -- * Documentation
    -- ** Error Generators
    ErrorGen(..),
    UnexpectedItem(..), 
    -- *** Blank Generators
    vanillaGen, specializedGen,
    -- ** Error Generating Combinators
    {-|
    These combinators create parsers that fail or raise errors with messages desribed by a given 'ErrorGen'.
    -}
    asFail, asSelect, asErr, 
  ) where
import Text.Gigaparsec.Internal (Parsec)
import Text.Gigaparsec.Internal qualified as Internal (Parsec(Parsec), State, specialisedErr, emptyErr, expectedErr, unexpectedErr, raise)
import Text.Gigaparsec.Internal.Errors qualified as Internal (Error, CaretWidth(RigidCaret), addReason)

{-|
This type describes special primitives that can use the results of a previous parser 
to form and raise an error message. 
This is not something that is normally possible with raw combinators, without using '(>>=)', 
which is expensive.

Primarily, these are designed to be used with 
'Text.Gigaparsec.Errors.Combinator.filterSWith'\/'Text.Gigaparsec.Errors.Patterns.verifiedWith'\/'Text.Gigaparsec.Errors.Patterns.preventWith'
but can be used in other parsers as well. 
-}
type ErrorGen :: * -> *
data ErrorGen a 
  -- | An error generator for /Specialized/ errors, which can tune the freeform messages of the error.
  = SpecializedGen { 
      -- | Produces the messages of the error message when given the result of the offending parser. 
      messages :: a -> [String] -- FIXME: 0.3.0.0 change to NonEmptyList.
      -- | Controls how wide an error is based on the value @a@ and width @Word@ provided.
    , adjustWidth :: a -> Word -> Word
    }
  -- | An error generator for /Vanilla/ errors, which can tune the unexpected message and a generated reason.
  | VanillaGen { 
      -- | Produces the unexpected component (if any) of the error message when given the result of the offending parser.
      unexpected :: a -> UnexpectedItem
      -- | Produces the reason component (if any) of the error message when given the result of the offending parser.
    , reason :: a -> Maybe String
    , adjustWidth :: a -> Word -> Word
    }

-- | A blank /Vanilla/ error generator, which does not affect the unexpected message or reason.
vanillaGen :: ErrorGen a
vanillaGen = VanillaGen { unexpected = const EmptyItem
                        , reason = const Nothing
                        , adjustWidth = const id
                        }

-- | The default /Specialized/ error generator, which does not affect the error message.
specializedGen :: ErrorGen a
specializedGen = SpecializedGen { messages = const []
                                , adjustWidth = const id
                                }

{-|
This type describes how to form the unexpected component of a vanilla error message from a 'VanillaGen'.

This includes the different sorts of 'unexpected item' messages that may occur;
whether to display the expected characters, a name for the expected expression, or not to display at all.
-}
type UnexpectedItem :: *
data UnexpectedItem 
  -- | The error should use whatever input was consumed by the offending parser, verbatim.
  = RawItem 
  -- | The error should not have an unexpected component at all (as in 'Text.Gigaparsec.filterS').
  | EmptyItem 
  -- | The error should use the given name as the unexpected component.
  | NamedItem String

{-|
Given a parser result and its width, raise an error according to the given error generator.
-}
asErr :: ErrorGen a -- ^ @errGen@, the generator for the error message to raise.
      -> a          -- ^ @x@, the result of the offending parser
      -> Word       -- ^ The width of the parsed result, @x@.
      -> Parsec b   -- ^ A parser that unconditionally raises an error described by @errGen@.
asErr errGen x w = Internal.raise $ \st -> genErr errGen st x w

{-|
This combinator takes a given parser @p@ and unconditionally fails with a message based on @p@'s results.
-}
asFail  :: ErrorGen a       -- ^ @errGen@, the generator for the error message.
        -> Parsec (a, Word) -- ^ @p@, a parser that returns a result @x@ and its width @w@.
        -> Parsec b         -- ^ A parser that unconditionally fails with a message described by @errGen@, 
                            --   using the result of @p@.
asFail errGen (Internal.Parsec p) = Internal.Parsec $ \st _ bad ->
  let good (x, w) st' = bad (genErr errGen st' x w) st'
  in  p st good bad

{-|
This combinator takes a given parser @p@ and, if @p@ returns an @a@, 
fails with a message based on this result.
-}
asSelect  :: ErrorGen a                  -- ^ @errGen@, the generator for the error message.
          -> Parsec (Either (a, Word) b) -- ^ @p@, a parser which may produce a bad result of type @a@
          -> Parsec b                    -- ^ A parser that fails if @p@ produces a bad result, 
                                         --   otherwise returns the result of @p@ if it is a @b@
asSelect errGen (Internal.Parsec p) = Internal.Parsec $ \st good bad ->
  let good' (Right x) st' = good x st'
      good' (Left (x, w)) st' = bad (genErr errGen st' x w) st'
  in p st good' bad

genErr :: ErrorGen a -> Internal.State -> a -> Word -> Internal.Error
genErr SpecializedGen{..} st x w =
  Internal.specialisedErr st (messages x) (Internal.RigidCaret (adjustWidth x w))
genErr VanillaGen{..} st x w =
  addReason (reason x) (makeError (unexpected x) st (adjustWidth x w))

makeError :: UnexpectedItem -> Internal.State -> Word -> Internal.Error
makeError RawItem st cw = Internal.expectedErr st [] cw
makeError EmptyItem st cw = Internal.emptyErr st cw
makeError (NamedItem name) st cw = Internal.unexpectedErr st [] name (Internal.RigidCaret cw)

-- no fold, unlifed type
addReason :: Maybe String -> Internal.Error -> Internal.Error
addReason Nothing err = err
addReason (Just reason) err = Internal.addReason reason err