gigaparsec 0.1.0.0 → 0.2.0.0
raw patch · 22 files changed
+2120/−369 lines, 22 filesdep +deepseqdep +gaugedep +tasty-expected-failurePVP ok
version bump matches the API change (PVP)
Dependencies added: deepseq, gauge, tasty-expected-failure
API changes (from Hackage documentation)
- Text.Gigaparsec: instance GHC.Classes.Eq a => GHC.Classes.Eq (Text.Gigaparsec.Result a)
- Text.Gigaparsec: instance GHC.Show.Show a => GHC.Show.Show (Text.Gigaparsec.Result a)
+ Text.Gigaparsec: instance (GHC.Classes.Eq a, GHC.Classes.Eq e) => GHC.Classes.Eq (Text.Gigaparsec.Result e a)
+ Text.Gigaparsec: instance (GHC.Show.Show a, GHC.Show.Show e) => GHC.Show.Show (Text.Gigaparsec.Result e a)
+ Text.Gigaparsec: parseRepl :: Show a => Parsec a -> String -> IO ()
+ Text.Gigaparsec: result :: (e -> b) -> (a -> b) -> Result e a -> b
+ Text.Gigaparsec.Errors.Combinator: amend :: Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: amendThenDislodge :: Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: amendThenDislodgeBy :: Word -> Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: dislodge :: Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: dislodgeBy :: Word -> Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: emptyWide :: Word -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: entrench :: Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: explain :: String -> Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: fail :: NonEmpty String -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: failWide :: Word -> NonEmpty String -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: hide :: Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: markAsToken :: Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: partialAmend :: Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: partialAmendThenDislodge :: Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: partialAmendThenDislodgeBy :: Word -> Parsec a -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: unexpected :: String -> Parsec a
+ Text.Gigaparsec.Errors.Combinator: unexpectedWide :: Word -> String -> Parsec a
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: StringBuilder :: (String -> String) -> StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: blockError :: StringBuilder -> [StringBuilder] -> Int -> StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: combineInfoWithLines :: [StringBuilder] -> [StringBuilder] -> [StringBuilder]
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: combineMessagesDefault :: Foldable t => t String -> [StringBuilder]
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: disjunct :: Bool -> [String] -> Maybe StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: endOfInputDefault :: String
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: expectedDefault :: Maybe StringBuilder -> Maybe StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: formatDefault :: StringBuilder -> Maybe StringBuilder -> [StringBuilder] -> String
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: formatPosDefault :: Word -> Word -> StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: from :: Show a => a -> StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: indentAndUnlines :: [StringBuilder] -> Int -> StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: instance Data.String.IsString Text.Gigaparsec.Errors.DefaultErrorBuilder.StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: instance GHC.Base.Monoid Text.Gigaparsec.Errors.DefaultErrorBuilder.StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: instance GHC.Base.Semigroup Text.Gigaparsec.Errors.DefaultErrorBuilder.StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: intercalate :: Monoid m => m -> [m] -> m
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: junct :: Bool -> [String] -> String -> Maybe StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: lineInfoDefault :: String -> [String] -> [String] -> Word -> Word -> [StringBuilder]
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: messageDefault :: String -> String
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: namedDefault :: String -> String
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: newtype StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: rawDefault :: String -> String
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: specialisedErrorDefault :: [StringBuilder] -> [StringBuilder] -> [StringBuilder]
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: toString :: StringBuilder -> String
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: unexpectedDefault :: Maybe String -> Maybe StringBuilder
+ Text.Gigaparsec.Errors.DefaultErrorBuilder: vanillaErrorDefault :: Foldable t => Maybe StringBuilder -> Maybe StringBuilder -> t StringBuilder -> [StringBuilder] -> [StringBuilder]
+ Text.Gigaparsec.Errors.ErrorBuilder: -- have prevented an error.
+ Text.Gigaparsec.Errors.ErrorBuilder: -- message.
+ Text.Gigaparsec.Errors.ErrorBuilder: -- must be orderable, as it is used within <tt>Set</tt>.
+ Text.Gigaparsec.Errors.ErrorBuilder: -- the parser.
+ Text.Gigaparsec.Errors.ErrorBuilder: -- | The type that represents the individual items within the error. It
+ Text.Gigaparsec.Errors.ErrorBuilder: Named :: !String -> {-# UNPACK #-} !Word -> Token
+ Text.Gigaparsec.Errors.ErrorBuilder: Raw :: !String -> Token
+ Text.Gigaparsec.Errors.ErrorBuilder: class (Ord (Item err)) => ErrorBuilder err where {
+ Text.Gigaparsec.Errors.ErrorBuilder: combineExpectedItems :: ErrorBuilder err => Set (Item err) -> ExpectedItems err
+ Text.Gigaparsec.Errors.ErrorBuilder: combineMessages :: ErrorBuilder err => [Message err] -> Messages err
+ Text.Gigaparsec.Errors.ErrorBuilder: data Token
+ Text.Gigaparsec.Errors.ErrorBuilder: endOfInput :: ErrorBuilder err => Item err
+ Text.Gigaparsec.Errors.ErrorBuilder: expected :: ErrorBuilder err => ExpectedItems err -> ExpectedLine err
+ Text.Gigaparsec.Errors.ErrorBuilder: format :: ErrorBuilder err => Position err -> Source err -> ErrorInfoLines err -> err
+ Text.Gigaparsec.Errors.ErrorBuilder: instance Text.Gigaparsec.Errors.ErrorBuilder.ErrorBuilder GHC.Base.String
+ Text.Gigaparsec.Errors.ErrorBuilder: lineInfo :: ErrorBuilder err => String -> [String] -> [String] -> Word -> Word -> LineInfo err
+ Text.Gigaparsec.Errors.ErrorBuilder: message :: ErrorBuilder err => String -> Message err
+ Text.Gigaparsec.Errors.ErrorBuilder: named :: ErrorBuilder err => String -> Item err
+ Text.Gigaparsec.Errors.ErrorBuilder: numLinesAfter :: ErrorBuilder err => Int
+ Text.Gigaparsec.Errors.ErrorBuilder: numLinesBefore :: ErrorBuilder err => Int
+ Text.Gigaparsec.Errors.ErrorBuilder: pos :: ErrorBuilder err => Word -> Word -> Position err
+ Text.Gigaparsec.Errors.ErrorBuilder: raw :: ErrorBuilder err => String -> Item err
+ Text.Gigaparsec.Errors.ErrorBuilder: reason :: ErrorBuilder err => String -> Message err
+ Text.Gigaparsec.Errors.ErrorBuilder: source :: ErrorBuilder err => Maybe FilePath -> Source err
+ Text.Gigaparsec.Errors.ErrorBuilder: specialisedError :: ErrorBuilder err => Messages err -> LineInfo err -> ErrorInfoLines err
+ Text.Gigaparsec.Errors.ErrorBuilder: type ErrorInfoLines err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type ExpectedItems err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type ExpectedLine err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type Item err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type LineInfo err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type Message err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type Messages err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type Position err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type Source err;
+ Text.Gigaparsec.Errors.ErrorBuilder: type UnexpectedLine err;
+ Text.Gigaparsec.Errors.ErrorBuilder: unexpected :: ErrorBuilder err => Maybe (Item err) -> UnexpectedLine err
+ Text.Gigaparsec.Errors.ErrorBuilder: unexpectedToken :: ErrorBuilder err => NonEmpty Char -> Word -> Bool -> Token
+ Text.Gigaparsec.Errors.ErrorBuilder: vanillaError :: ErrorBuilder err => UnexpectedLine err -> ExpectedLine err -> Messages err -> LineInfo err -> ErrorInfoLines err
+ Text.Gigaparsec.Errors.ErrorBuilder: }
- Text.Gigaparsec: Failure :: Result a
+ Text.Gigaparsec: Failure :: e -> Result e a
- Text.Gigaparsec: Success :: a -> Result a
+ Text.Gigaparsec: Success :: a -> Result e a
- Text.Gigaparsec: data Result a
+ Text.Gigaparsec: data Result e a
- Text.Gigaparsec: parse :: Parsec a -> String -> Result a
+ Text.Gigaparsec: parse :: forall err a. ErrorBuilder err => Parsec a -> String -> Result err a
Files
- CHANGELOG.md +7/−1
- benchmarks/Main.hs +21/−0
- gigaparsec.cabal +31/−3
- includes/portable-unlifted.h +25/−0
- src/Text/Gigaparsec.hs +35/−17
- src/Text/Gigaparsec/Char.hs +75/−65
- src/Text/Gigaparsec/Combinator.hs +90/−90
- src/Text/Gigaparsec/Errors/Combinator.hs +342/−6
- src/Text/Gigaparsec/Errors/DefaultErrorBuilder.hs +135/−0
- src/Text/Gigaparsec/Errors/ErrorBuilder.hs +379/−0
- src/Text/Gigaparsec/Internal.hs +65/−17
- src/Text/Gigaparsec/Internal/Errors.hs +271/−0
- test/Main.hs +2/−0
- test/Text/Gigaparsec/CharTests.hs +33/−33
- test/Text/Gigaparsec/CombinatorTests.hs +67/−67
- test/Text/Gigaparsec/ErrorsTests.hs +384/−0
- test/Text/Gigaparsec/Expr/ChainTests.hs +18/−18
- test/Text/Gigaparsec/Expr/InfixTests.hs +5/−5
- test/Text/Gigaparsec/ExprTests.hs +20/−20
- test/Text/Gigaparsec/Internal/Test.hs +46/−17
- test/Text/Gigaparsec/Internal/TestError.hs +59/−0
- test/Text/Gigaparsec/PrimitiveTests.hs +10/−10
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for gigaparsec -## 0.1.0.0 -- YYYY-mm-dd+## 0.1.0.0 -- 2023-10-17 * First version. Released on an unsuspecting world.++## 0.2.0.0 -- 2023-11-09++* Added error system.+* `parse` now has a type parameter, `parse @String` restores old behaviour+* for convenience `parseRepl` will print a parse to the terminal with the `String` error messages.
+ benchmarks/Main.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE Trustworthy #-}+{-# LANGUAGE StandaloneDeriving, DeriveAnyClass, DeriveGeneric #-}+{-# OPTIONS_GHC -Wno-orphans #-}+module Main (main) where++import Gauge (defaultMain, bench, nf)+import Text.Gigaparsec (Parsec, Result(Success, Failure), parse, atomic, (<|>))+import Text.Gigaparsec.Char (string)+import Control.DeepSeq (NFData)+import GHC.Generics (Generic)++p :: Parsec String+p = atomic (string "hello wold") <|> atomic (string "hi") <|> string "hello world"++deriving stock instance Generic (Result e a)+deriving anyclass instance (NFData a, NFData e) => NFData (Result e a)++main :: IO ()+main = defaultMain [+ bench "consumption" $ nf (parse @String p) "hello world"+ ]
gigaparsec.cabal view
@@ -20,7 +20,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.1.0.0+version: 0.2.0.0 -- A short (one-line) description of the package. synopsis:@@ -84,6 +84,8 @@ Text.Gigaparsec.Combinator, Text.Gigaparsec.Combinator.NonEmpty, Text.Gigaparsec.Errors.Combinator,+ Text.Gigaparsec.Errors.DefaultErrorBuilder,+ Text.Gigaparsec.Errors.ErrorBuilder, Text.Gigaparsec.Expr, Text.Gigaparsec.Expr.Chain, Text.Gigaparsec.Expr.Infix,@@ -92,6 +94,7 @@ -- Internals Text.Gigaparsec.Internal,+ Text.Gigaparsec.Internal.Errors, Text.Gigaparsec.Internal.RT, Text.Gigaparsec.Internal.Require @@ -107,6 +110,8 @@ -- Directories containing source files. hs-source-dirs: src+ include-dirs: includes+ install-includes: includes/portable-unlifted.h -- Base language which the package is written in. default-language: Haskell2010@@ -124,9 +129,11 @@ Text.Gigaparsec.CharTests, Text.Gigaparsec.CombinatorTests, Text.Gigaparsec.ExprTests,+ Text.Gigaparsec.ErrorsTests, Text.Gigaparsec.Expr.ChainTests, Text.Gigaparsec.Expr.InfixTests, Text.Gigaparsec.Internal.Test,+ Text.Gigaparsec.Internal.TestError, Text.Gigaparsec.Internal.PlainString ghc-options: -Wno-missing-export-lists -Wno-missing-safe-haskell-mode -Wno-safe@@ -146,11 +153,32 @@ build-depends: gigaparsec, containers >= 0.6 && < 0.7,+ --deriving-compat >= 0.6 && < 0.7, tasty >=1.1 && <1.6,- --tasty-expected-failure,- tasty-hunit >=0.9 && <0.11+ tasty-expected-failure >=0.11 && <0.13,+ tasty-hunit >=0.9 && <0.11, --TODO: property based testing will be useful when we optimise combinators -- to test against their base implementations --tasty-quickcheck --TODO: performance testing with tasty-bench?+ --tasty-bench -- we'd need to keep the basefile files somewhere, cache in CI or keep in repo?++benchmark perf-test+ import: warnings, extensions, base++ type: exitcode-stdio-1.0++ default-extensions: OverloadedStrings, TypeOperators, BlockArguments, GADTs++ -- Base language which the package is written in.+ default-language: Haskell2010++ hs-source-dirs: benchmarks+ build-depends:+ gigaparsec,+ --containers >= 0.6 && < 0.7,+ gauge >= 0.1 && < 0.3,+ deepseq >= 1.4 && < 1.6++ main-is: Main.hs
+ includes/portable-unlifted.h view
@@ -0,0 +1,25 @@+#ifdef false+/*+ * Copyright 2023 Gigaparsec Contributors <https://github.com/j-mie6/gigaparsec/graphs/contributors>+ *+ * SPDX-License-Identifier: BSD-3-Clause+ */++// This file enables the use of `UnliftedDatatypes` from 9.2 in a portable way+// include this file at the top underneath the requisite `CPP` extension, then+// have a `CPP_import_PortableUnlifted` import; `UnliftedDatatype` is now a kind+// that can be freely used.+#endif++#if __GLASGOW_HASKELL__ >= 902+{-# LANGUAGE UnliftedDatatypes #-}++#define CPP_import_PortableUnlifted import GHC.Exts (TYPE, RuntimeRep(BoxedRep), Levity(Unlifted))+#define UnliftedDatatype (TYPE ('BoxedRep 'Unlifted))++#else++#define CPP_import_PortableUnlifted+#define UnliftedDatatype *++#endif
src/Text/Gigaparsec.hs view
@@ -15,7 +15,7 @@ @since 0.1.0.0 -} module Text.Gigaparsec (- Parsec, Result(..), parse,+ Parsec, Result(..), result, parse, parseRepl, -- * Primitive Combinators -- | These combinators are specific to parser combinators. In one way or another, they influence -- how a parser consumes input, or under what conditions a parser does or does not fail. These are@@ -83,26 +83,43 @@ -- `Internal`: when they are in the public API, we are locked into them! import Text.Gigaparsec.Internal (Parsec(Parsec), emptyState, manyr, somer)-import Text.Gigaparsec.Internal qualified as Internal.State (State(..))-import Text.Gigaparsec.Internal.RT (runRT)+import Text.Gigaparsec.Internal qualified as Internal (State(..), useHints, expectedErr)+import Text.Gigaparsec.Internal.RT qualified as Internal (RT, runRT)+import Text.Gigaparsec.Internal.Errors qualified as Internal (ParseError, ExpectItem(ExpectEndOfInput), fromParseError) +import Text.Gigaparsec.Errors.ErrorBuilder (ErrorBuilder)+ import Data.Functor (void) import Control.Applicative (liftA2, (<|>), empty, many, some, (<**>)) -- liftA2 required until 9.6 import Control.Selective (select, branch) +import Data.Set qualified as Set (singleton, empty)+ -- Hiding the Internal module seems like the better bet: nobody needs to see it anyway :) -- re-expose like this to prevent hlint suggesting import refinement into internal --type Parsec :: * -> * --type Parsec = Internal.Parsec -type Result :: * -> *-data Result a = Success a | Failure deriving stock (Show, Eq)+type Result :: * -> * -> *+data Result e a = Success a | Failure e deriving stock (Show, Eq) -parse :: Parsec a -> String -> Result a-parse (Parsec p) inp = runRT $ p (emptyState inp) good bad- where good x _ = return (Success x)- bad _ = return Failure+result :: (e -> b) -> (a -> b) -> Result e a -> b+result _ success (Success x) = success x+result failure _ (Failure err) = failure err +{-# SPECIALISE parse :: Parsec a -> String -> Result String a #-}+{-# INLINABLE parse #-}+parse :: forall err a. ErrorBuilder err => Parsec a -> String -> Result err a+parse (Parsec p) inp = Internal.runRT $ p (emptyState inp) good bad+ where good :: a -> Internal.State -> Internal.RT (Result err a)+ good x _ = return (Success x)+ bad :: Internal.ParseError -> Internal.State -> Internal.RT (Result err a)+ bad err _ = return (Failure (Internal.fromParseError Nothing inp err))++-- TODO: documentation+parseRepl :: Show a => Parsec a -> String -> IO ()+parseRepl p inp = result putStrLn print (parse p inp)+ {-| This combinator parses its argument @p@, but rolls back any consumed input on failure. @@ -122,8 +139,7 @@ -} atomic :: Parsec a -- ^ the parser, @p@, to execute, if it fails, it will not have consumed input. -> Parsec a -- ^ a parser that tries @p@, but never consumes input if it fails.-atomic (Parsec p) = Parsec $ \st ok err ->- p st ok (const $ err st)+atomic (Parsec p) = Parsec $ \st ok bad -> p st ok (\err _ -> bad err st) {-| This combinator parses its argument @p@, but does not consume input if it succeeds. @@ -142,8 +158,7 @@ -} lookAhead :: Parsec a -- ^ the parser, @p@, to execute -> Parsec a -- ^ a parser that parses @p@ and never consumes input if it succeeds.-lookAhead (Parsec p) = Parsec $ \st ok err ->- p st (\x _ -> ok x st) err+lookAhead (Parsec p) = Parsec $ \st ok err -> p st (\x _ -> ok x st) err {-| This combinator parses its argument @p@, and succeeds when @p@ fails and vice-versa, never consuming@@ -168,8 +183,10 @@ -} notFollowedBy :: Parsec a -- ^ the parser, @p@, to execute, it must fail in order for this combinator to succeed. -> Parsec () -- ^ a parser which fails when @p@ succeeds and succeeds otherwise, never consuming input.-notFollowedBy (Parsec p) = Parsec $ \st ok err ->- p st (\_ _ -> err st) (\_ -> ok () st)+notFollowedBy (Parsec p) = Parsec $ \st ok bad ->+ p st (\_ st' -> let !width = Internal.consumed st' - Internal.consumed st+ in Internal.useHints bad (Internal.expectedErr st Set.empty width) st)+ (\_ _ -> ok () st) -- eof is usually `notFollowedBy item`, but this requires annoying cyclic dependencies on Char {- This parser only succeeds at the end of the input.@@ -185,8 +202,9 @@ @since 0.1.0.0 -} eof :: Parsec ()-eof = Parsec $ \st good bad -> case Internal.State.input st of- (:){} -> bad st+eof = Parsec $ \st good bad -> case Internal.input st of+ (:){} -> Internal.useHints bad+ (Internal.expectedErr st (Set.singleton Internal.ExpectEndOfInput) 1) st [] -> good () st {-|
src/Text/Gigaparsec/Char.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE Safe #-} {-# LANGUAGE OverloadedLists #-}-{-# OPTIONS_GHC -Wno-all-missed-specialisations #-}+{-# OPTIONS_GHC -Wno-all-missed-specialisations -Wno-overflowed-literals #-} {-| Module : Text.Gigaparsec.Char Description : Contains the combinators needed to read characters and strings, as well as combinators@@ -49,7 +49,8 @@ import Text.Gigaparsec.Combinator (skipMany) import Text.Gigaparsec.Errors.Combinator ((<?>)) -- We want to use this to make the docs point to the right definition for users.-import Text.Gigaparsec.Internal qualified as Internal (Parsec(Parsec), State(..))+import Text.Gigaparsec.Internal qualified as Internal (Parsec(Parsec, unParsec), State(..), expectedErr, useHints)+import Text.Gigaparsec.Internal.Errors qualified as Internal (ExpectItem(ExpectRaw), ParseError) import Text.Gigaparsec.Internal.Require (require) import Data.Bits (Bits((.&.), (.|.)))@@ -59,7 +60,7 @@ import Data.Maybe (isJust, fromJust) import Data.Monoid (Alt(Alt, getAlt)) import Data.Set (Set)-import Data.Set qualified as Set (member, size, findMin, findMax, mapMonotonic)+import Data.Set qualified as Set (empty, member, size, findMin, findMax, mapMonotonic, singleton) import Data.Map (Map) import Data.Map qualified as Map (fromSet, toAscList, member) @@ -67,6 +68,24 @@ -- Primitives ------------------------------------------------- +_satisfy :: Set Internal.ExpectItem -> (Char -> Bool) -> Parsec Char+_satisfy expecteds test = Internal.Parsec $ \st ok bad ->+ case Internal.input st of+ c:cs | test c -> ok c (updateState st c cs)+ _ -> Internal.useHints bad (Internal.expectedErr st expecteds 1) st+ where+ -- The duplicated input & consumed update avoids double allocation+ -- that occurs if they were done separately to the line and col updates.+ updateState st '\n' cs = st+ { Internal.line = Internal.line st + 1, Internal.col = 1,+ Internal.input = cs, Internal.consumed = Internal.consumed st + 1 }+ updateState st '\t' cs = st+ { Internal.col = ((Internal.col st + 3) .&. (-4)) .|. 1,+ Internal.input = cs, Internal.consumed = Internal.consumed st + 1 }+ updateState st _ cs = st+ { Internal.col = Internal.col st + 1,+ Internal.input = cs, Internal.consumed = Internal.consumed st + 1 }+ {-| This combinator tries to parse a single character from the input that matches the given predicate. @@ -75,11 +94,11 @@ consumed and this combinator will fail. ==== __Examples__->>> parse (satisfy Data.Char.isDigit) ""+>>> parse @String (satisfy Data.Char.isDigit) "" Failure ..->>> parse (satisfy Data.Char.isDigit) "7"+>>> parse @String (satisfy Data.Char.isDigit) "7" Success '7'->>> parse (satisfy Data.Char.isDigit) "a5"+>>> parse @String (satisfy Data.Char.isDigit) "a5" Failure .. Roughly speaking:@@ -94,23 +113,7 @@ -- exist. -> Parsec Char -- ^ a parser that tries to read a single character @c@, such that @pred c@ -- is true, or fails.-satisfy test = Internal.Parsec $ \st ok err ->- case Internal.input st of- c: cs | test c ->- ok c (updateState st c cs)- _ -> err st- where- -- The duplicated input & consumed update avoids double allocation- -- that occurs if they were done separately to the line and col updates.- updateState st '\n' cs = st- { Internal.line = Internal.line st + 1, Internal.col = 1,- Internal.input = cs, Internal.consumed = True }- updateState st '\t' cs = st- { Internal.col = ((Internal.col st + 3) .&. (-4)) .|. 1,- Internal.input = cs, Internal.consumed = True }- updateState st _ cs = st- { Internal.col = Internal.col st + 1,- Internal.input = cs, Internal.consumed = True }+satisfy = _satisfy Set.empty -- Needs to be primitive for the raw expected item down the line {-|@@ -121,18 +124,18 @@ combinator will fail. ==== __Examples__->>> parse (char 'a') ""+>>> parse @String (char 'a') "" Failure ..->>> parse (char 'a') "a"+>>> parse @String (char 'a') "a" Success 'a'->>> parse (char 'a') "ba"+>>> parse @String (char 'a') "ba" Failure .. @since 0.1.0.0 -} char :: Char -- ^ the character to parse, @c@. -> Parsec Char -- ^ a parser that tries to read a single @c@, or fails.-char c = satisfy (== c)+char c = _satisfy (Set.singleton (Internal.ExpectRaw (pure c))) (== c) -- Needs to be primitive for the raw expected item and wide caret down the line {-|@@ -145,11 +148,11 @@ matched are consumed from the input. ==== __Examples__->>> parse (string "abc") ""+>>> parse @String (string "abc") "" Failure ..->>> parse (string "abc") "abcd"+>>> parse @String (string "abc") "abcd" Success "abc"->>> parse (string "abc") "xabc"+>>> parse @String (string "abc") "xabc" Failure .. ==== Notes@@ -163,7 +166,12 @@ -> Parsec String -- ^ a parser that either parses the string @s@ or fails at the first -- mismatched character. string s = require (not (null s)) "Text.Gigaparsec.Char.string" "cannot pass empty string" $- traverse char s+ --TODO: this could be much improved+ Internal.Parsec $ \st ok bad ->+ let bad' (_ :: Internal.ParseError) =+ Internal.useHints bad (Internal.expectedErr st [Internal.ExpectRaw s]+ (fromIntegral (length s)))+ in Internal.unParsec (traverse char s) st ok bad' ------------------------------------------------- -- Composite Combinators@@ -179,11 +187,11 @@ ==== __Examples__ >>> let digit = satisfyMap (\c -> if isDigit c then Just (digitToInt c) else Nothing)->>> parse digit ""+>>> parse @String digit "" Failure ..->>> parse digit "7"+>>> parse @String digit "7" Success 7->>> parse digit "a5"+>>> parse @String digit "a5" Failure .. @since 0.1.0.0@@ -204,11 +212,11 @@ ==== __Examples__ >>> let p = oneOf (Set.fromList ['a'..'c'])->>> parse p "a"+>>> parse @String p "a" Success 'a'->>> parse p "c"+>>> parse @String p "c" Success 'c'->>> parse p "xb"+>>> parse @String p "xb" Failure .. @since 0.1.0.0@@ -220,14 +228,14 @@ | sz == 1 = char c1 -- if the smallest and largest characters are as far apart -- as the size of the set, it must be contiguous- | sz == (ord c2 - ord c1 + 1) = satisfy (\c -> c1 <= c && c <= c2) <?> Set.mapMonotonic show cs- | otherwise = satisfy (`Set.member` cs) <?> [rangeLabel]+ | sz == (ord c2 - ord c1 + 1) = satisfy (\c -> c1 <= c && c <= c2) <?> [rangeLabel]+ | otherwise = satisfy (`Set.member` cs) <?> Set.mapMonotonic (show . (: [])) cs where !sz = Set.size cs -- must be left lazy until sz known not to be 0 c1 = Set.findMin cs c2 = Set.findMax cs --FIXME: control character safe show (and for the map above!)- rangeLabel = "one of " ++ show c1 ++ " to " ++ show c2+ rangeLabel = "one of " ++ show @String [c1] ++ " to " ++ show @String [c2] {-| This combinator tries to parse any character __not__ from supplied set of characters @cs@,@@ -238,13 +246,13 @@ ==== __Examples__ >>> let p = noneOf (Set.from ['a'..'c'])->>> parse p "a"+>>> parse @String p "a" Failure ..->>> parse p "c"+>>> parse @String p "c" Failure ..->>> parse p "xb"+>>> parse @String p "xb" Success 'x'->>> parse p ""+>>> parse @String p "" Failure .. @since 0.1.0.0@@ -261,7 +269,7 @@ c1 = Set.findMin cs c2 = Set.findMax cs --FIXME: control character safe show- rangeLabel = "anything outside of " ++ show c1 ++ " to " ++ show c2+ rangeLabel = "anything outside of " ++ show @String [c1] ++ " to " ++ show @String [c2] {-| This combinator parses characters matching the given predicate __zero__ or more times, collecting@@ -273,11 +281,11 @@ ==== __Examples__ >>> let ident = letter <:> stringOfMany isAlphaNum->>> parse ident "abdc9d"+>>> parse @String ident "abdc9d" Success "abdc9d"->>> parse ident "a"+>>> parse @String ident "a" Success "a"->>> parser ident "9"+>>> parse @Stringr ident "9" Failure .. ==== Notes@@ -301,11 +309,11 @@ ==== __Examples__ >>> let ident = stringOfSome isAlpha->>> parse ident "abdc9d"+>>> parse @String ident "abdc9d" Success "abdc"->>> parse ident "a"+>>> parse @String ident "a" Success "a"->>> parser ident "9"+>>> parse @Stringr ident "9" Failure .. ==== Notes@@ -329,15 +337,15 @@ ==== __Examples__ >>> let p = strings (Set.fromList ["hell", "hello", "goodbye", "g", "abc"])->>> parse p "hell"+>>> parse @String p "hell" Success "hell"->>> parse p "hello"+>>> parse @String p "hello" Success "hello"->>> parse p "good"+>>> parse @String p "good" Success "g"->>> parse p "goodbye"+>>> parse @String p "goodbye" Success "goodbye"->>> parse p "a"+>>> parse @String p "a" Failure .. @since 0.1.0.0@@ -363,15 +371,15 @@ , ("g", pure 1) , ("abc", pure 3) ]->>> parse p "hell"+>>> parse @String p "hell" Success 4->>> parse p "hello"+>>> parse @String p "hello" Success 5->>> parse p "good"+>>> parse @String p "good" Success 1->>> parse p "goodbye"+>>> parse @String p "goodbye" Success 7->>> parse p "a"+>>> parse @String p "a" Failure .. ==== Notes@@ -496,8 +504,9 @@ An uppercase letter is any character whose Unicode /Category Type/ is Uppercase Letter (@Lu@). Examples of characters within this category include:- * the Latin letters @'A'@ through @'Z'@- * Latin special character such as @'Å'@, @'Ç'@, @'Õ'@++ * the Latin letters @\'A\'@ through @\'Z\'@+ * Latin special character such as @\'Å\'@, @\'Ç\'@, @\'Õ\'@ * Cryillic letters * Greek letters * Coptic letters@@ -514,8 +523,9 @@ Letter (@Ll@). Examples of characters within this category include:- * the Latin letters @'a'@ through @'z'@- * Latin special character such as @'é'@, @'ß'@, @'ð'@++ * the Latin letters @\'a\'@ through @\'z\'@+ * Latin special character such as @\'é\'@, @\'ß\'@, @\'ð\'@ * Cryillic letters * Greek letters * Coptic letters
src/Text/Gigaparsec/Combinator.hs view
@@ -1,6 +1,4 @@ {-# LANGUAGE Safe #-}-{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}-{-# HLINT ignore "Use optional" #-} {-| Module : Text.Gigaparsec.Combinator Description : This module contains a huge number of pre-made combinators that are@@ -67,13 +65,13 @@ ==== __Examples__ >>> let p = choice [string "abc", string "ab", string "bc", string "d"]->>> parse p "abc"+>>> parse @String p "abc" Success "abc"->>> parse p "ab"+>>> parse @String p "ab" Failure ..->>> parse p "bc"+>>> parse @String p "bc" Success "bc"->>> parse p "x"+>>> parse @String p "x" Failure .. @since 0.1.0.0@@ -91,9 +89,9 @@ ==== __Examples__ >>> let p = skip [char'a', item, char 'c']->>> parse p "abc"+>>> parse @String p "abc" Success ()->>> parse p "ab"+>>> parse @String p "ab" Failure .. @since 0.1.0.0@@ -110,11 +108,11 @@ ==== __Examples__ >>> let p = option (string "abc")->>> parse p ""+>>> parse @String p "" Success Nothing->>> parse p "abc"+>>> parse @String p "abc" Success (Just "abc")->>> parse p "ab"+>>> parse @String p "ab" Failure .. @since 0.1.0.0@@ -131,11 +129,11 @@ ==== __Examples__ >>> let p = optional (string "abc")->>> parse p ""+>>> parse @String p "" Success ()->>> parse p "abc"+>>> parse @String p "abc" Success ()->>> parse p "ab"+>>> parse @String p "ab" Failure .. @since 0.1.0.0@@ -152,11 +150,11 @@ ==== __Examples__ >>> let p = optionalAs 7 (string "abc")->>> parse p ""+>>> parse @String p "" Success 7->>> parse p "abc"+>>> parse @String p "abc" Success 7->>> parse p "ab"+>>> parse @String p "ab" Failure .. @since 0.1.0.0@@ -208,13 +206,13 @@ ==== __Examples__ >>> let p = manyN 2 (string "ab")->>> parse p ""+>>> parse @String p "" Failure ..->>> parse p "ab"+>>> parse @String p "ab" Failure ..->>> parse p "abababab"+>>> parse @String p "abababab" Success ["ab", "ab", "ab", "ab"]->>> parse p "aba"+>>> parse @String p "aba" Failure .. ==== Notes@@ -238,13 +236,13 @@ ==== __Examples__ >>> let p = skipMany (string "ab")->>> parse p ""+>>> parse @String p "" Success ()->>> parse p "ab"+>>> parse @String p "ab" Success ()->>> parse p "abababab"+>>> parse @String p "abababab" Success ()->>> parse p "aba"+>>> parse @String p "aba" Failure .. @since 0.1.0.0@@ -262,13 +260,13 @@ ==== __Examples__ >>> let p = skipSome (string "ab")->>> parse p ""+>>> parse @String p "" Failure ..->>> parse p "ab"+>>> parse @String p "ab" Success ()->>> parse p "abababab"+>>> parse @String p "abababab" Success ()->>> parse p "aba"+>>> parse @String p "aba" Failure .. @since 0.1.0.0@@ -286,13 +284,13 @@ ==== __Examples__ >>> let p = skipManyN 2 (string "ab")->>> parse p ""+>>> parse @String p "" Failure ..->>> parse p "ab"+>>> parse @String p "ab" Failure ..->>> parse p "abababab"+>>> parse @String p "abababab" Success ()->>> parse p "aba"+>>> parse @String p "aba" Failure .. @since 0.1.0.0@@ -313,13 +311,13 @@ ==== __Examples__ >>> let p = count (string "ab")->>> parse p ""+>>> parse @String p "" Success 0->>> parse p "ab"+>>> parse @String p "ab" Success 1->>> parse p "abababab"+>>> parse @String p "abababab" Success 4->>> parse p "aba"+>>> parse @String p "aba" Failure .. @since 0.1.0.0@@ -337,13 +335,13 @@ ==== __Examples__ >>> let p = count1 (string "ab")->>> parse p ""+>>> parse @String p "" Failure ..->>> parse p "ab"+>>> parse @String p "ab" Success 1->>> parse p "abababab"+>>> parse @String p "abababab" Success 4->>> parse p "aba"+>>> parse @String p "aba" Failure .. @since 0.1.0.0@@ -360,13 +358,13 @@ ==== __Examples__ >>> ... >>> let args = sepBy int (string ", ")->>> parse args "7, 3, 2"+>>> parse @String args "7, 3, 2" Success [7, 3, 2]->>> parse args ""+>>> parse @String args "" Success []->>> parse args "1"+>>> parse @String args "1" Success [1]->>> parse args "1, 2, "+>>> parse @String args "1, 2, " Failure .. @since 0.1.0.0@@ -387,13 +385,13 @@ ==== __Examples__ >>> ... >>> let args = sepBy1 int (string ", ")->>> parse args "7, 3, 2"+>>> parse @String args "7, 3, 2" Success [7, 3, 2]->>> parse args ""+>>> parse @String args "" Failure ..->>> parse args "1"+>>> parse @String args "1" Success [1]->>> parse args "1, 2, "+>>> parse @String args "1, 2, " Failure .. @since 0.1.0.0@@ -411,13 +409,13 @@ ==== __Examples__ >>> ... >>> let args = sepEndBy int (string ";\n")->>> parse args "7;\n3;\n2"+>>> parse @String args "7;\n3;\n2" Success [7, 3, 2]->>> parse args ""+>>> parse @String args "" Success Nil->>> parse args "1"+>>> parse @String args "1" Success [1]->>> parse args "1;\n2;\n"+>>> parse @String args "1;\n2;\n" Success [1, 2] @since 0.1.0.0@@ -438,13 +436,13 @@ ==== __Examples__ >>> ... >>> let args = sepEndBy1 int (string ";\n")->>> parse args "7;\n3;\n2"+>>> parse @String args "7;\n3;\n2" Success [7, 3, 2]->>> parse args ""+>>> parse @String args "" Failure ..->>> parse args "1"+>>> parse @String args "1" Success [1]->>> parse args "1;\n2;\n"+>>> parse @String args "1;\n2;\n" Success [1, 2] @since 0.1.0.0@@ -462,13 +460,13 @@ ==== __Examples__ >>> ... >>> let args = endBy int (string ";\n")->>> parse args "7;\n3;\n2"+>>> parse @String args "7;\n3;\n2" Failure ..->>> parse args ""+>>> parse @String args "" Success Nil->>> parse args "1;\n"+>>> parse @String args "1;\n" Success [1]->>> parse args "1;\n2;\n"+>>> parse @String args "1;\n2;\n" Success [1, 2] @since 0.1.0.0@@ -488,13 +486,13 @@ ==== __Examples__ >>> ... >>> let args = endBy1 int (string ";\n")->>> parse args "7;\n3;\n2"+>>> parse @String args "7;\n3;\n2" Failure ..->>> parse args ""+>>> parse @String args "" Failure ..->>> parse args "1;\n"+>>> parse @String args "1;\n" Success [1]->>> parse args "1;\n2;\n"+>>> parse @String args "1;\n2;\n" Success [1, 2] @since 0.1.0.0@@ -513,12 +511,13 @@ ==== __Examples__ This can be useful for scanning comments:+ >>> let comment = string "--" *> manyUntil item endOfLine->>> parse p "--hello world"+>>> parse @String p "--hello world" Failure ..->>> parse p "--hello world\n"+>>> parse @String p "--hello world\n" Success ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']->>> parse p "--\n"+>>> parse @String p "--\n" Success Nil @since 0.1.0.0@@ -538,14 +537,15 @@ ==== __Examples__ This can be useful for scanning comments:+ >>> let comment = string "--" *> someUntil item endOfLine->>> parse p "--hello world"+>>> parse @String p "--hello world" Failure ..->>> parse p "--hello world\n"+>>> parse @String p "--hello world\n" Success ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']->>> parse p "--\n"+>>> parse @String p "--\n" Failure ..->>> parse p "--a\n"+>>> parse @String p "--a\n" Success ['a'] @since 0.1.0.0@@ -646,11 +646,11 @@ ==== __Examples__ >>> let p = exactly 3 item->>> parse p "ab"+>>> parse @String p "ab" Failure ..->>> parse p "abc"+>>> parse @String p "abc" Success ['a', 'b', 'c']->>> parse p "abcd"+>>> parse @String p "abcd" Success ['a', 'b', 'c'] @since 0.1.0.0@@ -669,15 +669,15 @@ ==== __Examples__ >>> let p = range 3 5 item->>> parse p "ab"+>>> parse @String p "ab" Failure ..->>> parse p "abc"+>>> parse @String p "abc" Success ['a', 'b', 'c']->>> parse p "abcd"+>>> parse @String p "abcd" Success ['a', 'b', 'c', 'd']->>> parse p "abcde"+>>> parse @String p "abcde" Success ['a', 'b', 'c', 'd', 'e']->>> parse p "abcdef"+>>> parse @String p "abcdef" Success ['a', 'b', 'c', 'd', 'e'] @since 0.1.0.0@@ -703,15 +703,15 @@ ==== __Examples__ >>> let p = range_ 3 5 item->>> parse p "ab"+>>> parse @String p "ab" Failure ..->>> parse p "abc"+>>> parse @String p "abc" Success ()->>> parse p "abcd"+>>> parse @String p "abcd" Success ()->>> parse p "abcde"+>>> parse @String p "abcde" Success ()->>> parse p "abcdef"+>>> parse @String p "abcdef" Success () @since 0.1.0.0@@ -739,15 +739,15 @@ ==== __Examples__ >>> let p = count 3 5 item->>> parse p "ab"+>>> parse @String p "ab" Failure ..->>> parse p "abc"+>>> parse @String p "abc" Success 3->>> parse p "abcd"+>>> parse @String p "abcd" Success 4->>> parse p "abcde"+>>> parse @String p "abcde" Success 5->>> parse p "abcdef"+>>> parse @String p "abcdef" Success 5 @since 0.1.0.0
src/Text/Gigaparsec/Errors/Combinator.hs view
@@ -1,18 +1,354 @@ {-# LANGUAGE Safe #-}-module Text.Gigaparsec.Errors.Combinator (module Text.Gigaparsec.Errors.Combinator) where+{-# OPTIONS_GHC -Wno-missing-import-lists #-}+{-|+Module : Text.Gigaparsec.Errors.Combinator+Description : This module contains combinators that can be used to directly influence error+ messages of parsers.+License : BSD-3-Clause+Maintainer : Jamie Willis, Gigaparsec Maintainers+Stability : stable +Error messages are, by default, not /particularly/ descriptive. However, the combinators in this+module can be used to improve the generation of error messages by providing labels for expected+items, explanations for why things went wrong, custom error messages, custom unexpected error messages,+as well as correcting the offsets that error messages actually occurred at.++==== Terminology++__Observably consumes input__: a parser is said to /observably/ consume input when error messages generated by a parser @p@ occur at a deeper+offset than @p@ originally started at. While this sounds like it is the same as "having consumed input" for the+purposes of backtracking, they are disjoint concepts:++ 1. in @atomic p@, @p@ can /observably/ consume input even though the wider parser does not consume input due to the @atomic@.+ 2. in @amend p@, @p@ can consume input and may not backtrack even though the consumption is not /observable/ in the error+ message due to the @amend@.++@since 0.2.0.0+-}+module Text.Gigaparsec.Errors.Combinator (+ -- * Error Enrichment Combinators+ -- | These combinators add additional information - or refine the existing information within - to+ -- an error message that has been generated within the scope of the parser they have been called on.+ -- These are a very basic, but effective, way of improving the quality of error messages generated+ -- by gigaparsec.+ label, (<?>), hide, explain,+ -- * Failure Combinators+ -- | These combinators immediately fail the parser, with a more bespoke message.+ emptyWide,+ fail, failWide,+ unexpected, unexpectedWide,+ -- * Error Adjustment Combinators+ -- | These combinators can affect at what position an error is caused at. They are+ -- opposites: where 'amend' will ensure an error message is said to have generated+ -- at the position on entry to the combinator, 'entrench' will resist these changes.+ amend, partialAmend, entrench, dislodge, dislodgeBy,+ amendThenDislodge, amendThenDislodgeBy, partialAmendThenDislodge, partialAmendThenDislodgeBy,+ markAsToken+ ) where++{-+Future doc headings:++Filtering Combinators+=====================+These combinators perform filtering on a parser, with particular emphasis on generating meaningful+error messages if the filtering fails. This is particularly useful for data validation within the+parser, as very instructive error messages describing what went wrong can be generated. These combinators+often filter using a `PartialFunction`: this may be because they combine filtering with mapping (in which+case, the error message is provided separately), or the function may produce a `String`.++In these cases, the partial function is producing the error messages: if the input to the function is+defined, this means that it is invalid and the filtering will fail using the message obtained from the+successful partial function invocation.++Generic Filtering Combinators+=============================+This combinators generalise the combinators from above, which are all special cases of them. Each of these+takes the characteristic predicate or function of the regular variants, but takes an `errGen` object that+can be used to fine-tune the error messages. These offer some flexiblity not offered by the specialised+filtering combinators, but are a little more verbose to use.+-}++import Prelude hiding (fail)+ import Text.Gigaparsec (Parsec) -- We want to use this to make the docs point to the right definition for users.---import Text.Gigaparsec.Internal qualified as Internal (Parsec(Parsec))+import Text.Gigaparsec.Internal qualified as Internal (Parsec(Parsec), line, col, emptyErr, specialisedErr, raise, unexpectedErr, hints, consumed, useHints, adjustErr, hints, hintsValidOffset)+import Text.Gigaparsec.Internal.Errors (ParseError, CaretWidth(FlexibleCaret, RigidCaret), ExpectItem(ExpectNamed))+import Text.Gigaparsec.Internal.Errors qualified as Internal (setLexical, amendErr, entrenchErr, dislodgeErr, partialAmendErr, labelErr, explainErr) import Text.Gigaparsec.Internal.Require (require) import Data.Set (Set)+import Data.Set qualified as Set (empty, map)+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NonEmpty (toList) --- the empty set is weird here, do we require non-empty or just make it id?-label :: Set String -> Parsec a -> Parsec a-label ls =- require (not (any null ls)) "Text.Gigaparsec.Errors.Combinator.label" "labels cannot be empty" id --TODO:+{-|+This combinator changes the expected component of any errors generated by this parser. +When this parser fails having not /observably/ consumed input, the expected component of the generated+error message is set to be the given items.+-}+label :: Set String -- ^ the names to give to the expected component of any qualifying errors.+ -> Parsec a -- ^ the parser to apply the labels to+ -> Parsec a+label ls (Internal.Parsec p) =+ require (not (null ls) && not (any null ls)) "Text.Gigaparsec.Errors.Combinator.label"+ "labels cannot be empty" $+ Internal.Parsec $ \st good bad ->+ let !origConsumed = Internal.consumed st+ good' x st'+ | Internal.consumed st' /= origConsumed = good x st'+ | otherwise = good x st' { Internal.hints = Set.map ExpectNamed ls }+ bad' err = Internal.useHints bad (Internal.labelErr origConsumed ls err)+ in p st good' bad'++{-|+This combinator suppresses the entire error message generated by a given parser.++When this parser fails having not /observably/ consumed input, this combinator+replaces any error generated by the given parser to match the 'Text.Gigaparsec.empty' combinator.++This can be useful, say, for hiding whitespace labels, which are not normally useful+information to include in an error message for whitespace insensitive grammars.+-}+hide :: Parsec a -> Parsec a+hide (Internal.Parsec p) =+ Internal.Parsec $ \st good bad ->+ let !origConsumed = Internal.consumed st+ good' x st' = good x st' { Internal.hints = Set.empty }+ bad' err st'+ | Internal.consumed st' /= origConsumed = bad err st'+ | otherwise = Internal.useHints bad (Internal.emptyErr st' 0) st'+ in p st good' bad'++{-|+This combinator adds a reason to error messages generated by this parser.++When this parser fails having not /observably/ consumed input, this combinator adds+a reason to the error message, which should justify why the error occured. Unlike error+labels, which may persist if more progress is made having not consumed input, reasons+are not carried forward in the error message, and are lost.+-}+explain :: String -- ^ reason the reason why a parser failed.+ -> Parsec a -- ^ the parser to apply the reason to+ -> Parsec a+explain reason (Internal.Parsec p) =+ Internal.Parsec $ \st good bad ->+ let !origConsumed = Internal.consumed st+ bad' err = Internal.useHints bad (Internal.explainErr origConsumed reason err)+ in p st good bad'++{-|+This combinator fails immediately, with a caret of the given width and no other information.++By producing basically no information, this combinator is principally for adjusting the+caret-width of another error, rather than the value 'Text.Gigaparsec.empty', which is used to fail with+no effect on error content.+-}+emptyWide :: Word -- ^ the width of the caret for the error produced by this combinator.+ -> Parsec a+emptyWide width = Internal.raise (`Internal.emptyErr` width)++{-|+This combinator consumes no input and fails immediately with the given error messages.++Produces a /specialised/ error message where all the lines of the error are the+given @msgs@ in order of appearance.++==== __Examples__+>>> let failing = fail ["hello,", "this is an error message", "broken across multiple lines"]++-}+fail :: NonEmpty String -- ^ the messages that will make up the error message.+ -> Parsec a+fail = _fail (FlexibleCaret 1)++{-|+This combinator consumes no input and fails immediately with the given error messages.++Produces a /specialised/ error message where all the lines of the error are the+given @msgs@ in order of appearance. The caret width of the message is set to the+given value.++==== __Examples__+>>> let failing = fail 3 ["hello,", "this is an error message", "broken across multiple lines"]++-}+failWide :: Word -- ^ the width of the caret for the error produced by this combinator.+ -> NonEmpty String -- ^ the messages that will make up the error message.+ -> Parsec a+failWide width = _fail (RigidCaret width)++{-# INLINE _fail #-}+_fail :: CaretWidth -> NonEmpty String -> Parsec a+_fail width msgs = Internal.raise (\st -> Internal.specialisedErr st (NonEmpty.toList msgs) width)++{-|+This combinator consumes no input and fails immediately, setting the unexpected component+to the given item.++Produces a /trivial/ error message where the unexpected component of the error is+replaced with the given item.+-}+unexpected :: String -- ^ the unexpected message for the error generated.+ -> Parsec a+unexpected = _unexpected (FlexibleCaret 1)++{-|+This combinator consumes no input and fails immediately, setting the unexpected component+to the given item.++Produces a /trivial/ error message where the unexpected component of the error is+replaced with the given item. The caret width of the message is set to the+given value.+-}+unexpectedWide :: Word -- ^ the width of the caret for the error produced by this combinator.+ -> String -- ^ the unexpected message for the error generated.+ -> Parsec a+unexpectedWide width = _unexpected (RigidCaret width)++{-# INLINE _unexpected #-}+_unexpected :: CaretWidth -> String -> Parsec a+_unexpected width name = Internal.raise $ \st -> Internal.unexpectedErr st Set.empty name width++{-|+This combinator adjusts any error messages generated by the given parser so that they+occur at the position recorded on entry to this combinator (effectively as if no+input were consumed).++This is useful if validation work is done+on the output of a parser that may render it invalid, but the error should point to the+beginning of the structure. This combinators effect can be cancelled with [[entrench `entrench`]].++==== __Examples__+>>> let greeting = string "hello world" <* char '!'+>>> parseRepl (greeting <?> ["greeting"]) "hello world."+(line 1, column 12):+ unexpected "."+ expected "!"+ >hello world.+ ^+>>> parseRepl (amend greeting <?> ["greeting"]) "hello world."+(line 1, column 1):+ unexpected "h"+ expected greeting+ >hello world.+ ^+-}+amend :: Parsec a -> Parsec a+amend = _amend Internal.amendErr++--TODO: examples+{-|+This combinator adjusts any error messages generated by the given parser so that they+occur at the position recorded on entry to this combinator, but retains the original offset.++Similar to 'amend', but retains the original offset the error occurred at. This is known+as its /underlying offset/ as opposed to the visual /presentation offset/. To the reader, the+error messages appears as if no input was consumed, but for the purposes of error message merging+the error is still deeper. A key thing to note is that two errors can only merge if they are at+the same presentation /and/ underlying offsets: if they are not the deeper of the two /dominates/.++The ability for an error to still dominate others after partial amendment can be useful for allowing+it to avoid being lost when merging with errors that are deeper than the presentation offset but+shallower than the underlying.+-}+partialAmend :: Parsec a -> Parsec a+partialAmend = _amend Internal.partialAmendErr++{-# INLINE _amend #-}+_amend :: (Word -> Word -> Word -> ParseError -> ParseError) -> Parsec a -> Parsec a+_amend f (Internal.Parsec p) =+ Internal.Parsec $ \st good bad ->+ let !origConsumed = Internal.consumed st+ !origLine = Internal.line st+ !origCol = Internal.col st+ !origHints = Internal.hints st+ !origHintsValidOffset = Internal.hintsValidOffset st+ in p st good $ \err st' -> bad (f origConsumed origLine origCol err)+ st' { Internal.hints = origHints+ , Internal.hintsValidOffset = origHintsValidOffset }++--TODO: examples+{-|+This combinator prevents the action of any enclosing 'amend' on the errors generated by the given+parser.++Sometimes, the error adjustments performed by 'amend' should only affect errors generated+within a certain part of a parser and not the whole thing; in this case, 'entrench' can be used+to protect sub-parsers from having their errors adjusted, providing a much more fine-grained+scope for error adjustment.+-}+entrench :: Parsec a -> Parsec a+entrench = Internal.adjustErr Internal.entrenchErr++{-|+This combinator undoes the action of any 'entrench' combinators on the given parser.++Entrenchment is important for preventing the incorrect amendment of certain parts of sub-errors+for a parser, but it may be then undesireable to block further amendments from elsewhere in the+parser. This combinator can be used to cancel all entrenchment after the critical section has+passed.+-}+dislodge :: Parsec a -> Parsec a+dislodge = dislodgeBy maxBound++{-|+This combinator undoes the action of the given number of 'entrench' combinators on the given parser.++Entrenchment is important for preventing the incorrect amendment of certain parts of sub-errors+for a parser, but it may be then undesireable to block further amendments from elsewhere in the+parser. This combinator can be used to cancel all entrenchment after the critical section has+passed.+-}+dislodgeBy :: Word -> Parsec a -> Parsec a+dislodgeBy by = Internal.adjustErr (Internal.dislodgeErr by)++{-|+This combinator first tries to amend the position of any error generated by the given parser,+and if the error was entrenched will dislodge it instead.+-}+amendThenDislodge :: Parsec a -> Parsec a+amendThenDislodge = dislodge . amend++{-|+This combinator first tries to amend the position of any error generated by the given parser,+and if the error was entrenched will dislodge it the given number of times instead.+-}+amendThenDislodgeBy :: Word -> Parsec a -> Parsec a+amendThenDislodgeBy n = dislodgeBy n . amend++{-|+This combinator first tries to partially amend the position of any error generated by the given parser,+and if the error was entrenched will dislodge it instead.+-}+partialAmendThenDislodge :: Parsec a -> Parsec a+partialAmendThenDislodge = dislodge . partialAmend++{-|+This combinator first tries to partially amend the position of any error generated by the given parser,+and if the error was entrenched will dislodge it the given number of times instead.+-}+partialAmendThenDislodgeBy :: Word -> Parsec a -> Parsec a+partialAmendThenDislodgeBy n = dislodgeBy n . partialAmend++{-|+This combinator marks any errors within the given parser as being /lexical errors/.++When an error is marked as a /lexical error/, it sets a flag within the error that is+passed to 'Text.Gigaparsec.Errors.ErrorBuilder.unexpectedToken': this+should be used to prevent @Lexer@-based token extraction from being performed on an error,+since lexing errors cannot be the result of unexpected tokens.+-}+markAsToken :: Parsec a -> Parsec a+markAsToken = Internal.adjustErr Internal.setLexical++{-|+This combinator changes the expected component of any errors generated by this parser.++This is just an alias for the 'label' combinator.+-} {-# INLINE (<?>) #-} infix 0 <?> (<?>) :: Parsec a -> Set String -> Parsec a
+ src/Text/Gigaparsec/Errors/DefaultErrorBuilder.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE Trustworthy #-}+{-# LANGUAGE DerivingVia, OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-missing-import-lists #-}+module Text.Gigaparsec.Errors.DefaultErrorBuilder (module Text.Gigaparsec.Errors.DefaultErrorBuilder) where++import Prelude hiding (lines)++import Data.Monoid (Endo(Endo))+import Data.String (IsString(fromString))+import Data.List (intersperse, sortBy)+import Data.Maybe (mapMaybe)+import Data.Foldable (toList)+import Data.Ord (comparing, Down (Down))++-- For now, this is the home of the default formatting functions++type StringBuilder :: *+newtype StringBuilder = StringBuilder (String -> String)+ deriving (Semigroup, Monoid) via Endo String++instance IsString StringBuilder where+ {-# INLINE fromString #-}+ fromString :: String -> StringBuilder+ fromString str = StringBuilder (str ++)++{-# INLINE toString #-}+toString :: StringBuilder -> String+toString (StringBuilder build) = build mempty++{-# INLINE from #-}+from :: Show a => a -> StringBuilder+from = StringBuilder . shows++{-# INLINABLE formatDefault #-}+formatDefault :: StringBuilder -> Maybe StringBuilder -> [StringBuilder] -> String+formatDefault pos source lines = toString (blockError header lines 2)+ where header = maybe mempty (\src -> "In " <> src <> " ") source <> pos++{-# INLINABLE vanillaErrorDefault #-}+vanillaErrorDefault :: Foldable t => Maybe StringBuilder -> Maybe StringBuilder -> t StringBuilder -> [StringBuilder] -> [StringBuilder]+vanillaErrorDefault unexpected expected reasons =+ combineInfoWithLines (maybe id (:) unexpected (maybe id (:) expected (toList reasons)))++{-# INLINABLE specialisedErrorDefault #-}+specialisedErrorDefault :: [StringBuilder] -> [StringBuilder] -> [StringBuilder]+specialisedErrorDefault = combineInfoWithLines++{-# INLINABLE combineInfoWithLines #-}+combineInfoWithLines :: [StringBuilder] -> [StringBuilder] -> [StringBuilder]+combineInfoWithLines [] lines = "unknown parse error" : lines+combineInfoWithLines info lines = info ++ lines++--TODO: this needs to deal with whitespace and unprintables+{-# INLINABLE rawDefault #-}+rawDefault :: String -> String+rawDefault n = "\"" <> n <> "\""++{-# INLINABLE namedDefault #-}+namedDefault :: String -> String+namedDefault = id++{-# INLINABLE endOfInputDefault #-}+endOfInputDefault :: String+endOfInputDefault = "end of input"++{-# INLINABLE messageDefault #-}+messageDefault :: String -> String+messageDefault = id++{-# INLINABLE expectedDefault #-}+expectedDefault :: Maybe StringBuilder -> Maybe StringBuilder+expectedDefault = fmap ("expected " <>)++{-# INLINABLE unexpectedDefault #-}+unexpectedDefault :: Maybe String -> Maybe StringBuilder+unexpectedDefault = fmap (("unexpected " <>) . fromString)++{-# INLINABLE disjunct #-}+disjunct :: Bool -> [String] -> Maybe StringBuilder+disjunct oxford elems = junct oxford elems "or"++{-# INLINABLE junct #-}+junct :: Bool -> [String] -> String -> Maybe StringBuilder+junct oxford elems junction = junct' (sortBy (comparing Down) elems)+ where+ j :: StringBuilder+ j = fromString junction++ junct' [] = Nothing+ junct' [alt] = Just (fromString alt)+ junct' [alt1, alt2] = Just (fromString alt2 <> " " <> fromString junction <> " " <> fromString alt1)+ junct' as@(alt:alts)+ -- use a semi-colon here, it is more correct+ | any (elem ',') as = Just (junct'' (reverse alts) alt "; ")+ | otherwise = Just (junct'' (reverse alts) alt ", ")++ junct'' is l delim = front <> back+ where front = intercalate (fromString delim) (map fromString is) :: StringBuilder+ back+ | oxford = fromString delim <> j <> " " <> fromString l+ | otherwise = " " <> j <> " " <> fromString l++{-# INLINABLE combineMessagesDefault #-}+combineMessagesDefault :: Foldable t => t String -> [StringBuilder]+combineMessagesDefault = mapMaybe (\msg -> if null msg then Nothing else Just (fromString msg)) . toList++{-# INLINABLE blockError #-}+blockError :: StringBuilder -> [StringBuilder] -> Int -> StringBuilder+blockError header lines indent = header <> ":\n" <> indentAndUnlines lines indent++{-# INLINABLE indentAndUnlines #-}+indentAndUnlines :: [StringBuilder] -> Int -> StringBuilder+indentAndUnlines lines indent = fromString pre <> intercalate (fromString ('\n' : pre)) lines+ where pre = replicate indent ' '++{-# INLINABLE lineInfoDefault #-}+lineInfoDefault :: String -> [String] -> [String] -> Word -> Word -> [StringBuilder]+lineInfoDefault curLine beforeLines afterLines pointsAt width =+ concat [map inputLine beforeLines, [inputLine curLine, caretLine], map inputLine afterLines]+ where inputLine :: String -> StringBuilder+ inputLine = fromString . ('>' :)+ caretLine :: StringBuilder+ caretLine = fromString (replicate (fromIntegral (pointsAt + 1)) ' ') <> fromString (replicate (fromIntegral width) '^')++{-# INLINABLE formatPosDefault #-}+formatPosDefault :: Word -> Word -> StringBuilder+formatPosDefault line col = "(line "+ <> from line+ <> ", column "+ <> from col+ <> ")"++{-# INLINABLE intercalate #-}+intercalate :: Monoid m => m -> [m] -> m+intercalate x xs = mconcat (intersperse x xs)
+ src/Text/Gigaparsec/Errors/ErrorBuilder.hs view
@@ -0,0 +1,379 @@+{-# LANGUAGE Safe #-}+{-# LANGUAGE TypeFamilies, AllowAmbiguousTypes, FlexibleInstances, FlexibleContexts #-}+{-|+Module : Text.Gigaparsec.Errors.ErrorBuilder+Description : This typeclass specifies how to format an error from a parser as a specified type.+License : BSD-3-Clause+Maintainer : Jamie Willis, Gigaparsec Maintainers+Stability : stable++This typeclass specifies how to format an error from a parser+as a specified type.++An instance of this typeclass is required when calling 'Text.Gigaparsec.parse'+(or similar). By default, @gigaparsec@ defines its own instance for+@ErrorBuilder String@ found in this module.++To implement @ErrorBuilder@, a number of methods must be defined,+as well the representation types for a variety of different components;+the relation between the various methods is closely linked+to the types that they both produce and consume. To only change+the basics of formatting without having to define the entire instance,+use the methods found in "Text.Gigaparsec.Errors.DefaultErrorBuilder".++= How an Error is Structured+There are two kinds of error messages that are generated by @gigaparsec@:+/Specialised/ and /Vanilla/. These are produced by different combinators+and can be merged with other errors of the same type if both errors appear+at the same offset. However, /Specialised/ errors will take precedence+over /Vanilla/ errors if they appear at the same offset. The most+common form of error is the /Vanilla/ variant, which is generated by+most combinators, except for some in "Text.Gigaparsec.Errors.Combinator".++Both types of error share some common structure, namely:++ - The error preamble, which has the file and the position.+ - The content lines, the specifics of which differ between the two types of error.+ - The context lines, which has the surrounding lines of input for contextualisation.++== /Vanilla/ Errors+There are three kinds of content line found in a /Vanilla/ error:++ 1. Unexpected info: this contains information about the kind of token that caused the error.+ 2. Expected info: this contains the information about what kinds of token could have avoided the error.+ 3. Reasons: these are the bespoke reasons that an error has occurred (as generated by 'Text.Gigaparsec.Errors.Combinator.explain').++There can be at most one unexpected line, at most one expected line, and zero or more reasons.+Both of the unexpected and expected info are built up of /error items/, which are either:+the end of input, a named token, raw input taken from the parser definition. These can all be+formatted separately.++The overall structure of a /Vanilla/ error is given in the following diagram:++> ┌───────────────────────────────────────────────────────────────────────┐+> │ Vanilla Error │+> │ ┌────────────────┐◄──────── position │+> │ source │ │ │+> │ │ │ line col│ │+> │ ▼ │ │ ││ │+> │ ┌─────┐ │ ▼ ▼│ end of input │+> │ In foo.txt (line 1, column 5): │ │+> │ ┌─────────────────────┐ │ │+> │unexpected ─────►│ │ │ ┌───── expected │+> │ │ ┌──────────┐ ◄──────────┘ │ │+> │ unexpected end of input ▼ │+> │ ┌──────────────────────────────────────┐ │+> │ expected "(", "negate", digit, or letter │+> │ │ └──────┘ └───┘ └────┘ ◄────── named│+> │ │ ▲ └──────────┘ │ │+> │ │ │ │ │+> │ │ raw │ │+> │ └─────────────────┬───────────┘ │+> │ '-' is a binary operator │ │+> │ └──────────────────────┘ │ │+> │ ┌──────┐ ▲ │ │+> │ │>3+4- │ │ expected items │+> │ │ ^│ │ │+> │ └──────┘ └───────────────── reason │+> │ ▲ │+> │ │ │+> │ line info │+> └───────────────────────────────────────────────────────────────────────┘+++== /Specialised/ Errors+There is only one kind of content found in a /Specialised/ error:+a message. These are completely free-form, and are generated by the+'Text.Gigaparsec.Errors.Combinator.failWide' combinator, as well as its derived combinators.+There can be one or more messages in a /Specialised/ error.++The overall structure of a /Specialised/ error is given in the following diagram:++> ┌───────────────────────────────────────────────────────────────────────┐+> │ Specialised Error │+> │ ┌────────────────┐◄──────── position │+> │ source │ │ │+> │ │ │ line col │+> │ ▼ │ │ │ │+> │ ┌─────┐ │ ▼ ▼ │+> │ In foo.txt (line 1, column 5): │+> │ │+> │ ┌───► something went wrong │+> │ │ │+> │ message ──┼───► it looks like a binary operator has no argument │+> │ │ │+> │ └───► '-' is a binary operator │+> │ ┌──────┐ │+> │ │>3+4- │ │+> │ │ ^│ │+> │ └──────┘ │+> │ ▲ │+> │ │ │+> │ line info │+> └───────────────────────────────────────────────────────────────────────┘++@since 0.2.0.0+-}+module Text.Gigaparsec.Errors.ErrorBuilder (ErrorBuilder(..), Token(..)) where++import Text.Gigaparsec.Errors.DefaultErrorBuilder ( StringBuilder, formatDefault+ , vanillaErrorDefault, specialisedErrorDefault+ , rawDefault, namedDefault, endOfInputDefault+ , expectedDefault, unexpectedDefault+ , disjunct, combineMessagesDefault+ , formatPosDefault, lineInfoDefault+ )++import Data.Char (isSpace, generalCategory, ord, GeneralCategory(Format, Surrogate, PrivateUse, NotAssigned, Control))+import Data.Kind (Constraint)+import Data.List.NonEmpty (NonEmpty((:|)))+import Data.Set (Set)+import Data.Set qualified as Set (toList)+import Data.String (IsString(fromString))+import Numeric (showHex)++{-|+This class describes how to format an error message generated by a parser into+a form the parser writer desires.+-}+type ErrorBuilder :: * -> Constraint+class (Ord (Item err)) => ErrorBuilder err where+ {-|+ This is the top level function, which finally compiles all the formatted+ sub-parts into a finished value of type @err@.+ -}+ format :: Position err -- ^ the representation of the position of the error in the input (see the 'pos' method).+ -> Source err -- ^ the representation of the filename, if it exists (see the 'source' method).+ -> ErrorInfoLines err -- ^ the main body of the error message (see 'vanillaError' or 'specialisedError' methods).+ -> err -- ^ the final error message++ -- | The representation type of position information within the generated message.+ type Position err+ -- | The representation of the file information.+ type Source err+ {-|+ Formats a position into the representation type given by 'Position'.+ -}+ pos :: Word -- ^ the line the error occurred at.+ -> Word -- ^ the column the error occurred at.+ -> Position err -- ^ a representation of the position.+ {-|+ Formats the name of the file parsed from, if it exists, into the type given by 'Source'.+ -}+ source :: Maybe FilePath -- ^ the source name of the file, if any.+ -> Source err++ -- | The representation type of the main body within the error message.+ type ErrorInfoLines err+ {-|+ Vanilla errors are those produced such that they have information about+ both @expected@ and @unexpected@ tokens. These are usually the default,+ and are not produced by @fail@ (or any derivative) combinators.+ -}+ vanillaError :: UnexpectedLine err -- ^ information about which token(s) caused the error (see the 'unexpected' method).+ -> ExpectedLine err -- ^ information about which token(s) would have avoided the error (see the 'expected' method).+ -> Messages err -- ^ additional information about why the error occured (see the 'combineMessages' method).+ -> LineInfo err -- ^ representation of the line of input that this error occured on (see the 'lineInfo' method).+ -> ErrorInfoLines err+ {-|+ Specialised errors are triggered by @fail@ and any combinators that are+ implemented in terms of @fail@. These errors take precedence over+ the vanilla errors, and contain less, more specialised, information.+ -}+ specialisedError :: Messages err -- ^ information detailing the error (see the 'combineMessages' method).+ -> LineInfo err -- ^ representation of the line of input that this error occured on (see the 'lineInfo' method).+ -> ErrorInfoLines err++ -- | The representation of all the different possible tokens that could have prevented an error.+ type ExpectedItems err+ -- | The representation of the combined reasons or failure messages from the parser.+ type Messages err++ {-|+ Details how to combine the various expected items into a single representation.+ -}+ combineExpectedItems :: Set (Item err) -- ^ the possible items that fix the error.+ -> ExpectedItems err+ {-|+ Details how to combine any reasons or messages generated within a+ single error. Reasons are used by @vanilla@ messages and messages+ are used by @specialised@ messages.+ -}+ combineMessages :: [Message err] -- ^ the messages to combine (see the 'message' or 'reason' methods).+ -> Messages err++ -- | The representation of the information regarding the problematic token.+ type UnexpectedLine err+ -- | The representation of the information regarding the solving tokens.+ type ExpectedLine err+ -- | The representation of a reason or a message generated by the parser.+ type Message err+ -- | The representation of the line of input where the error occurred.+ type LineInfo err++ {-|+ Describes how to handle the (potentially missing) information+ about what token(s) caused the error.+ -}+ unexpected :: Maybe (Item err) -- ^ the @Item@ that caused this error.+ -> UnexpectedLine err+ {-|+ Describes how to handle the information about the tokens that+ could have avoided the error.+ -}+ expected :: ExpectedItems err -- ^ the tokens that could have prevented the error (see 'combineExpectedItems').+ -> ExpectedLine err+ {-|+ Describes how to represent the reasons behind a parser fail.+ These reasons originate from the 'Text.Gigaparsec.Errors.Combinator.explain' combinator.+ -}+ reason :: String -- ^ the reason produced by the parser.+ -> Message err+ {-|+ Describes how to represent the messages produced by the+ 'Text.Gigaparsec.Errors.Combinator.fail' combinator (or any that are implemented using it).+ -}+ message :: String -- ^ the message produced by the parser.+ -> Message err++ {-|+ Describes how to format the information about the line that the error occured on,+ and its surrounding context.+ -}+ lineInfo :: String -- ^ the full line of input that produced this error message.+ -> [String] -- ^ the lines of input from just before the one that produced this message (up to 'numLinesBefore').+ -> [String] -- ^ the lines of input from just after the one that produced this message (up to 'numLinesAfter').+ -> Word -- ^ the offset into the line that the error points at.+ -> Word -- ^ how wide the caret in the message should be.+ -> LineInfo err++ -- | The number of lines of input to request before an error occured.+ numLinesBefore :: Int+ -- | The number of lines of input to request after an error occured.+ numLinesAfter :: Int++ -- | The type that represents the individual items within the error. It must be+ -- orderable, as it is used within @Set@.+ type Item err++ {-|+ Formats a raw item generated by either the input string or a input+ reading combinator without a label.+ -}+ raw :: String -- ^ the raw, unprocessed input.+ -> Item err+ -- | Formats a named item generated by a label.+ named :: String -- ^ the name given to the label.+ -> Item err+ -- | Value that represents the end of the input in the error message.+ endOfInput :: Item err++ {-|+ Extracts an unexpected token from the remaining input.++ When a parser fails, by default an error reports an unexpected token of a specific width.+ This works well for some parsers, but often it is nice to have the illusion of a dedicated+ lexing pass: instead of reporting the next few characters as unexpected, an unexpected token+ can be reported instead. This can take many forms, for instance trimming the token to the+ next whitespace, only taking one character, or even trying to lex a token out of the stream.++ TODO: talk about the token extractors when they are added.+ -}+ unexpectedToken :: NonEmpty Char -- ^ the remaining input, @cs@, at point of failure.+ -> Word -- ^ the input the parser tried to read when it failed+ -- (this is __not__ guaranteed to be smaller than the length of+ -- @cs@, but is __guaranteed to be greater than 0__).+ -> Bool -- ^ was this error generated as part of \"lexing\", or in a wider parser (see 'Text.Gigaparsec.Errors.Combinator.markAsToken').+ -> Token -- ^ a token extracted from @cs@ that will be used as part of the unexpected message.++{-|+This type represents an extracted token returned by 'unexpectedToken' in 'ErrorBuilder'.++There is deliberately no analogue for @EndOfInput@ because we guarantee that non-empty+residual input is provided to token extraction.+-}+type Token :: *+data Token = Raw -- ^ This is a token that is directly extracted from the residual input itself.+ !String -- ^ the input extracted.+ | Named -- ^ This is a token that has been given a name, and is treated like a labelled item.+ !String -- ^ the description of the token.+ {-# UNPACK #-} !Word -- ^ the amount of residual input this token ate.++{-|+Formats error messages as a string, using the functions found in+"Text.Gigaparsec.Errors.DefaultErrorBuilder".+-}+instance ErrorBuilder String where+ {-# INLINE format #-}+ format = formatDefault++ type Position String = StringBuilder+ type Source String = Maybe StringBuilder++ {-# INLINE pos #-}+ pos = formatPosDefault+ {-# INLINE source #-}+ source = fmap fromString++ type ErrorInfoLines String = [StringBuilder]+ {-# INLINE vanillaError #-}+ vanillaError = vanillaErrorDefault+ {-# INLINE specialisedError #-}+ specialisedError = specialisedErrorDefault++ type ExpectedItems String = Maybe StringBuilder+ type Messages String = [StringBuilder]++ {-# INLINE combineExpectedItems #-}+ combineExpectedItems = disjunct True . Set.toList+ {-# INLINE combineMessages #-}+ combineMessages = combineMessagesDefault++ type UnexpectedLine String = Maybe StringBuilder+ type ExpectedLine String = Maybe StringBuilder+ type Message String = String+ type LineInfo String = [StringBuilder]++ {-# INLINE unexpected #-}+ unexpected = unexpectedDefault+ {-# INLINE expected #-}+ expected = expectedDefault+ {-# INLINE reason #-}+ reason = id+ {-# INLINE message #-}+ message = id++ {-# INLINE lineInfo #-}+ lineInfo = lineInfoDefault++ {-# INLINE numLinesBefore #-}+ numLinesBefore = 1+ {-# INLINE numLinesAfter #-}+ numLinesAfter = 1++ type Item String = String++ {-# INLINE raw #-}+ raw = rawDefault+ {-# INLINE named #-}+ named = namedDefault+ {-# INLINE endOfInput #-}+ endOfInput = endOfInputDefault++ {-# INLINABLE unexpectedToken #-}+ -- TillNextWhitespace with matches parser demand+ unexpectedToken ('\n' :| _) _ _ = Named "newline" 1+ unexpectedToken ('\r' :| _) _ _ = Named "carriage return" 1+ unexpectedToken ('\t' :| _) _ _ = Named "tab" 1+ unexpectedToken (' ' :| _) _ _ = Named "space" 1+ unexpectedToken (c :| cs) parserDemanded _+ | isSpace c = Named "whitespace character" 1+ | otherwise = case generalCategory c of+ Format -> unprintable+ Surrogate -> unprintable+ PrivateUse -> unprintable+ NotAssigned -> unprintable+ Control -> unprintable+ _ -> Raw (take (fromIntegral parserDemanded) (tillNextWhitespace (c:cs)))+ where unprintable = Named ("non-printable character (\\x" ++ showHex (ord c) ")") 1+ tillNextWhitespace = takeWhile (not . isSpace)
src/Text/Gigaparsec/Internal.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE Trustworthy #-}-{-# LANGUAGE DeriveFunctor, StandaloneDeriving #-}+{-# LANGUAGE DeriveFunctor, StandaloneDeriving, RecordWildCards, NamedFieldPuns, CPP #-}+#include "portable-unlifted.h" {-# OPTIONS_HADDOCK hide #-} {-| Module : Text.Gigaparsec.Internal@@ -17,10 +18,20 @@ module Text.Gigaparsec.Internal (module Text.Gigaparsec.Internal) where import Text.Gigaparsec.Internal.RT (RT)+import Text.Gigaparsec.Internal.Errors (ParseError, ExpectItem, CaretWidth)+import Text.Gigaparsec.Internal.Errors qualified as Errors (+ emptyErr, expectedErr, specialisedErr, mergeErr, unexpectedErr,+ expecteds, isExpectedEmpty, presentationOffset, useHints+ ) import Control.Applicative (Applicative(liftA2), Alternative(empty, (<|>), many, some)) -- liftA2 required until 9.6 import Control.Selective (Selective(select)) +import Data.Set (Set)+import Data.Set qualified as Set (empty, union)++CPP_import_PortableUnlifted+ {- Notes: @@ -38,7 +49,7 @@ newtype Parsec a = Parsec { unParsec :: forall r. State -> (a -> State -> RT r) -- the good continuation- -> (State -> RT r) -- the bad continuation+ -> (ParseError -> State -> RT r) -- the bad continuation -> RT r } @@ -106,21 +117,23 @@ {-# INLINE return #-} {-# INLINE (>>=) #-} +raise :: (State -> ParseError) -> Parsec a+raise mkErr = Parsec $ \st _ bad -> useHints bad (mkErr st) st+ instance Alternative Parsec where empty :: Parsec a- empty = Parsec $ \st _ err -> err st+ empty = raise (`emptyErr` 0) + -- FIXME: I feel like there is something missing here with hint merging from ctx.mergeHints+ -- if the hint stack is not real then it lives in the continuation trace, but I don't know... which (<|>) :: Parsec a -> Parsec a -> Parsec a- Parsec p <|> Parsec q = Parsec $ \st ok err ->- let !initConsumed = consumed st- ok' x st' = ok x (st' { consumed = initConsumed || consumed st' })- -- ^ revert to old st.consumed if p didn't consume- err' st'- | consumed st' = err st'+ Parsec p <|> Parsec q = Parsec $ \st ok bad ->+ let bad' err st'+ | consumed st' > consumed st = bad err st' -- ^ fail if p failed *and* consumed- | otherwise = q (st' { consumed = initConsumed }) ok err-- in p (st { consumed = False }) ok' err'+ | otherwise = q st' (\x st'' -> ok x (errorToHints st'' err))+ (\err' -> bad (Errors.mergeErr err err'))+ in p st ok bad' many :: Parsec a -> Parsec [a] many = manyr (:) []@@ -153,21 +166,56 @@ {-# INLINE mempty #-} -type State :: *+type State :: UnliftedDatatype data State = State { -- | the input string, in future this may be generalised input :: !String, -- | has the parser consumed input since the last relevant handler?- consumed :: !Bool, -- this could be an Int offset instead, perhaps?+ consumed :: {-# UNPACK #-} !Word, -- | the current line number (incremented by \n)- line :: {-# UNPACK #-} !Int,+ line :: {-# UNPACK #-} !Word, -- | the current column number (have to settle on a tab handling scheme)- col :: {-# UNPACK #-} !Int+ col :: {-# UNPACK #-} !Word,+ -- | the valid for which hints can be used+ hintsValidOffset :: {-# UNPACK #-} !Word,+ -- | the hints at this point in time+ hints :: !(Set ExpectItem) } emptyState :: String -> State emptyState !str = State { input = str- , consumed = False+ , consumed = 0 , line = 1 , col = 1+ , hintsValidOffset = 0+ , hints = Set.empty }++emptyErr :: State -> Word -> ParseError+emptyErr State{..} = Errors.emptyErr consumed line col++expectedErr :: State -> Set ExpectItem -> Word -> ParseError+expectedErr State{..} = Errors.expectedErr input consumed line col++specialisedErr :: State -> [String] -> CaretWidth -> ParseError+specialisedErr State{..} = Errors.specialisedErr consumed line col++unexpectedErr :: State -> Set ExpectItem -> String -> CaretWidth -> ParseError+unexpectedErr State{..} = Errors.unexpectedErr consumed line col++errorToHints :: State -> ParseError -> State+errorToHints st@State{..} err+ | consumed == Errors.presentationOffset err+ , not (Errors.isExpectedEmpty err) =+ if hintsValidOffset < consumed then st { hints = Errors.expecteds err, hintsValidOffset = consumed }+ else st { hints = Set.union hints (Errors.expecteds err) }+errorToHints st _ = st++useHints :: (ParseError -> State -> RT r) -> (ParseError -> State -> RT r)+useHints bad err st@State{hintsValidOffset, hints}+ | presentationOffset == hintsValidOffset = bad (Errors.useHints hints err) st+ | otherwise = bad err st{ hintsValidOffset = presentationOffset, hints = Set.empty }+ where !presentationOffset = Errors.presentationOffset err++adjustErr :: (ParseError -> ParseError) -> Parsec a -> Parsec a+adjustErr f (Parsec p) = Parsec $ \st good bad -> p st good $ \err -> bad (f err)
+ src/Text/Gigaparsec/Internal/Errors.hs view
@@ -0,0 +1,271 @@+{-# LANGUAGE Trustworthy #-}+{-# LANGUAGE RecordWildCards, BangPatterns, NamedFieldPuns, CPP #-}+#include "portable-unlifted.h"+{-# OPTIONS_GHC -Wno-partial-fields -Wno-all-missed-specialisations #-}+{-# OPTIONS_HADDOCK hide #-}+{-# OPTIONS_GHC -Wno-missing-import-lists #-}+module Text.Gigaparsec.Internal.Errors (module Text.Gigaparsec.Internal.Errors) where++import Prelude hiding (lines)++import Data.List.NonEmpty (NonEmpty((:|)), nonEmpty, (<|))+import Data.Set (Set)+import Data.Set qualified as Set (empty, map, union, null, foldr, insert)++import Text.Gigaparsec.Errors.ErrorBuilder (ErrorBuilder, Token)+import Text.Gigaparsec.Errors.ErrorBuilder qualified as Builder (ErrorBuilder(..))+import Text.Gigaparsec.Errors.ErrorBuilder qualified as Token (Token(..))++CPP_import_PortableUnlifted++type Span :: *+type Span = Word++type CaretWidth :: UnliftedDatatype+data CaretWidth = FlexibleCaret { width :: {-# UNPACK #-} !Span }+ | RigidCaret { width :: {-# UNPACK #-} !Span }++isFlexible :: CaretWidth -> Bool+isFlexible FlexibleCaret{} = True+isFlexible _ = False++type ParseError :: UnliftedDatatype+data ParseError = VanillaError { presentationOffset :: {-# UNPACK #-} !Word+ , line :: {-# UNPACK #-} !Word+ , col :: {-# UNPACK #-} !Word+ , unexpected :: !(Either Word UnexpectItem) -- TODO: unlift this!+ -- sadly, this prevents unlifting of ExpectItem+ -- perhaps we should make an unlifted+levity polymorphic Set?+ , expecteds :: !(Set ExpectItem)+ , reasons :: !(Set String)+ , lexicalError :: !Bool -- TODO: strict bools+ -- TODO: remove:+ , underlyingOffset :: {-# UNPACK #-} !Word+ , entrenchment :: {-# UNPACK #-} !Word+ }+ | SpecialisedError { presentationOffset :: {-# UNPACK #-} !Word+ , line :: {-# UNPACK #-} !Word+ , col :: {-# UNPACK #-} !Word+ , msgs :: ![String]+ --, caretWidth :: {-# UNPACK #-} !Span --FIXME: need defunc before this goes away+ , caretWidth :: CaretWidth+ -- TODO: remove:+ , underlyingOffset :: {-# UNPACK #-} !Word+ , entrenchment :: {-# UNPACK #-} !Word+ }++type Input :: *+type Input = NonEmpty Char+type UnexpectItem :: *+data UnexpectItem = UnexpectRaw !Input {-# UNPACK #-} !Word+ | UnexpectNamed !String CaretWidth+ | UnexpectEndOfInput+type ExpectItem :: *+data ExpectItem = ExpectRaw !String+ | ExpectNamed !String+ | ExpectEndOfInput+ deriving stock (Eq, Ord, Show)++entrenched :: ParseError -> Bool+entrenched err = entrenchment err /= 0++emptyErr :: Word -> Word -> Word -> Word -> ParseError+emptyErr !presentationOffset !line !col !width = VanillaError {+ presentationOffset = presentationOffset,+ line = line,+ col = col,+ unexpected = Left width,+ expecteds = Set.empty,+ reasons = Set.empty,+ lexicalError = False,+ underlyingOffset = presentationOffset,+ entrenchment = 0+ }++expectedErr :: String -> Word -> Word -> Word -> Set ExpectItem -> Word -> ParseError+expectedErr !input !presentationOffset !line !col !expecteds !width = VanillaError {+ presentationOffset = presentationOffset,+ line = line,+ col = col,+ unexpected = case nonEmpty input of+ Nothing -> Right UnexpectEndOfInput+ Just cs -> Right (UnexpectRaw cs width),+ expecteds = expecteds,+ reasons = Set.empty,+ lexicalError = False,+ underlyingOffset = presentationOffset,+ entrenchment = 0+}++specialisedErr :: Word -> Word -> Word -> [String] -> CaretWidth -> ParseError+specialisedErr !presentationOffset !line !col !msgs caretWidth = SpecialisedError {..}+ where !underlyingOffset = presentationOffset+ !entrenchment = 0 :: Word++unexpectedErr :: Word -> Word -> Word -> Set ExpectItem -> String -> CaretWidth -> ParseError+unexpectedErr !presentationOffset !line !col !expecteds !name caretWidth = VanillaError {+ presentationOffset = presentationOffset,+ line = line,+ col = col,+ expecteds = expecteds,+ unexpected = Right (UnexpectNamed name caretWidth),+ reasons = Set.empty,+ lexicalError = False,+ underlyingOffset = presentationOffset,+ entrenchment = 0+ }++labelErr :: Word -> Set String -> ParseError -> ParseError+labelErr !offset expecteds err@VanillaError{}+ | offset == presentationOffset err = err { expecteds = Set.map ExpectNamed expecteds }+labelErr _ _ err = err++explainErr :: Word -> String -> ParseError -> ParseError+explainErr !offset reason err@VanillaError{}+ | offset == presentationOffset err = err { reasons = Set.insert reason (reasons err) }+explainErr _ _ err = err++amendErr :: Word -> Word -> Word -> ParseError -> ParseError+amendErr !offset !line !col err+ | not (entrenched err) = err {+ presentationOffset = offset,+ underlyingOffset = offset,+ line = line,+ col = col+ }+amendErr _ _ _ err = err++partialAmendErr :: Word -> Word -> Word -> ParseError -> ParseError+partialAmendErr !offset !line !col err+ | not (entrenched err) = err {+ presentationOffset = offset,+ line = line,+ col = col+ }+partialAmendErr _ _ _ err = err++entrenchErr :: ParseError -> ParseError+entrenchErr err = err { entrenchment = entrenchment err + 1 }++dislodgeErr :: Word -> ParseError -> ParseError+dislodgeErr by err+ | entrenchment err == 0 = err+ -- this case is important to avoid underflow on the unsigned Word+ | by >= entrenchment err = err { entrenchment = 0 }+ | otherwise = err { entrenchment = entrenchment err - by }++setLexical :: ParseError -> ParseError+setLexical err@VanillaError{} = err { lexicalError = True }+setLexical err = err++useHints :: Set ExpectItem -> ParseError -> ParseError+useHints !hints err@VanillaError{expecteds} = err { expecteds = Set.union hints expecteds }+useHints _ err = err++mergeErr :: ParseError -> ParseError -> ParseError+mergeErr err1 err2+ | underlyingOffset err1 > underlyingOffset err2 = err1+ | underlyingOffset err1 < underlyingOffset err2 = err2+ | presentationOffset err1 > presentationOffset err2 = err1+ | presentationOffset err1 < presentationOffset err2 = err2+-- offsets are all equal, kinds must match+mergeErr err1@SpecialisedError{caretWidth} _err2@VanillaError{}+ | isFlexible caretWidth = err1 -- TODO: flexible caret merging from err2+ | otherwise = err1+mergeErr _err1@VanillaError{} err2@SpecialisedError{caretWidth}+ | isFlexible caretWidth = err2 -- TODO: flexible caret merging from err1+ | otherwise = err2+mergeErr err1@VanillaError{} err2@VanillaError{} =+ err1 { unexpected = mergeUnexpect (unexpected err1) (unexpected err2)+ , expecteds = Set.union (expecteds err1) (expecteds err2)+ , reasons = Set.union (reasons err1) (reasons err2)+ , lexicalError = lexicalError err1 || lexicalError err2+ }+mergeErr err1@SpecialisedError{} err2@SpecialisedError{} =+ err1 { msgs = msgs err1 ++ msgs err2+ , caretWidth = mergeCaret (caretWidth err1) (caretWidth err2)+ }++mergeCaret :: CaretWidth -> CaretWidth -> CaretWidth+mergeCaret caret@RigidCaret{} FlexibleCaret{} = caret+mergeCaret FlexibleCaret{} caret@RigidCaret{} = caret+mergeCaret caret1 caret2 = caret1 { width = max (width caret1) (width caret2) }++mergeUnexpect :: Either Word UnexpectItem -> Either Word UnexpectItem -> Either Word UnexpectItem+mergeUnexpect (Left w1) (Left w2) = Left (max w1 w2)+-- TODO: widening can occur with flexible or raw tokens+mergeUnexpect Left{} w@Right{} = w+mergeUnexpect w@Right{} Left{} = w+-- finally, two others will merge independently+mergeUnexpect (Right item1) (Right item2) = Right (mergeItem item1 item2)+ where mergeItem UnexpectEndOfInput _ = UnexpectEndOfInput+ mergeItem _ UnexpectEndOfInput = UnexpectEndOfInput+ mergeItem it1@(UnexpectNamed _ cw1) it2@(UnexpectNamed _ cw2)+ | isFlexible cw1, not (isFlexible cw2) = it2+ | not (isFlexible cw1), isFlexible cw2 = it1+ | width cw1 < width cw2 = it2+ | otherwise = it1+ mergeItem item@UnexpectNamed{} _ = item+ mergeItem _ item@UnexpectNamed{} = item+ mergeItem (UnexpectRaw cs w1) (UnexpectRaw _ w2) = UnexpectRaw cs (max w1 w2)++isExpectedEmpty :: ParseError -> Bool+isExpectedEmpty VanillaError{expecteds} = Set.null expecteds+isExpectedEmpty _ = True++{-# INLINABLE fromParseError #-}+fromParseError :: forall err. ErrorBuilder err => Maybe FilePath -> String -> ParseError -> err+fromParseError srcFile input err =+ Builder.format (Builder.pos @err (line err) (col err)) (Builder.source @err srcFile)+ (formatErr err)+ where formatErr :: ParseError -> Builder.ErrorInfoLines err+ formatErr VanillaError{..} =+ Builder.vanillaError @err+ (Builder.unexpected @err (either (const Nothing) (Just . fst) unexpectedTok))+ (Builder.expected @err (Builder.combineExpectedItems @err (Set.map expectItem expecteds)))+ (Builder.combineMessages @err (Set.foldr (\r -> (Builder.reason @err r :)) [] reasons))+ (Builder.lineInfo @err curLine linesBefore linesAfter caret (trimToLine caretSize))+ where unexpectedTok = unexpectItem lexicalError <$> unexpected+ caretSize = either id snd unexpectedTok++ formatErr SpecialisedError{..} =+ Builder.specialisedError @err+ (Builder.combineMessages @err (map (Builder.message @err) msgs))+ (Builder.lineInfo @err curLine linesBefore linesAfter caret (trimToLine (width caretWidth)))++ expectItem :: ExpectItem -> Builder.Item err+ expectItem (ExpectRaw t) = Builder.raw @err t+ expectItem (ExpectNamed n) = Builder.named @err n+ expectItem ExpectEndOfInput = Builder.endOfInput @err++ unexpectItem :: Bool -> UnexpectItem -> (Builder.Item err, Span)+ unexpectItem lexical (UnexpectRaw cs demanded) =+ case Builder.unexpectedToken @err cs demanded lexical of+ t@(Token.Raw tok) -> (Builder.raw @err tok, tokenSpan t)+ Token.Named name w -> (Builder.named @err name, w)+ unexpectItem _ (UnexpectNamed name caretWidth) = (Builder.named @err name, width caretWidth)+ unexpectItem _ UnexpectEndOfInput = (Builder.endOfInput @err, 1)++ -- it is definitely the case that there are at least `line` lines+ (allLinesBefore, curLine, allLinesAfter) = breakLines (line err - 1) (lines input)+ linesBefore = drop (length allLinesBefore - Builder.numLinesBefore @err) allLinesBefore+ linesAfter = take (Builder.numLinesAfter @err) allLinesAfter++ caret = col err - 1+ trimToLine width = min width (fromIntegral (length curLine) - caret + 1)++ lines :: String -> NonEmpty String+ lines [] = "" :| []+ lines ('\n':cs) = "" <| lines cs+ lines (c:cs) = let l :| ls = lines cs in (c:l) :| ls++ breakLines :: Word -> NonEmpty String -> ([String], String, [String])+ breakLines 0 (l :| ls) = ([], l, ls)+ breakLines n (l :| ls) = case nonEmpty ls of+ Nothing -> error "the focus line is guaranteed to exist"+ Just ls' -> let (before, focus, after) = breakLines (n - 1) ls'+ in (l : before, focus, after)++ tokenSpan :: Token -> Word+ tokenSpan (Token.Raw cs) = fromIntegral (length cs)+ tokenSpan (Token.Named _ w) = w
test/Main.hs view
@@ -6,6 +6,7 @@ import Text.Gigaparsec.CharTests qualified as Char import Text.Gigaparsec.CombinatorTests qualified as Combinator import Text.Gigaparsec.ExprTests qualified as Expr+import Text.Gigaparsec.ErrorsTests qualified as Errors main :: IO () main = defaultMain $ testGroup "gigaparsec"@@ -13,4 +14,5 @@ , Char.tests , Combinator.tests , Expr.tests+ , Errors.tests ]
test/Text/Gigaparsec/CharTests.hs view
@@ -33,9 +33,9 @@ pureParseWith (satisfy (== 'a')) "b" , testCase "be impure otherwise" do impureParseWith (satisfy (== 'a')) "a" , testCase "return the parsed character" do- parse item "a" @?= Success 'a'- parse item "ba" @?= Success 'b'- parse item "\NUL" @?= Success '\NUL'+ testParse item "a" @?= Success 'a'+ testParse item "ba" @?= Success 'b'+ testParse item "\NUL" @?= Success '\NUL' , testCase "fail otherwise" do ensureFails (satisfy (== 'b')) "" ensureFails item ""@@ -48,7 +48,7 @@ pureParseWith (char 'a') "" pureParseWith (char 'a') "b" , testCase "be impure otherwise" do impureParseWith (char 'a') "a"- , testCase "return the parsed character" do parse (char 'a') "a" @?= Success 'a'+ , testCase "return the parsed character" do testParse (char 'a') "a" @?= Success 'a' , testCase "fail otherwise" do ensureFails (char 'a') "" ensureFails (char 'a') "b"@@ -56,13 +56,13 @@ stringTests :: TestTree stringTests = testGroup "string should"- [ testCase "reject the empty string" (throws @RequirementUnsatisfied (parse (string "") "")) -- don't ask why `string ""` doesn't work all the time+ [ testCase "reject the empty string" (throws @RequirementUnsatisfied (testParse (string "") "")) -- don't ask why `string ""` doesn't work all the time , testCase "be pure if it cannot read at all" do pureParseWith (string "abc") "" pureParseWith (string "abc") "123" , testCase "be impure if there is a partial read" do impureParseWith (string "abc") "abd" , testCase "be impure if full read" do impureParseWith (string "abc") "abc"- , testCase "return the parsed string" do parse (string "123") "123" @?= Success "123"+ , testCase "return the parsed string" do testParse (string "123") "123" @?= Success "123" , testCase "fail otherwise" do ensureFails (string "123") "124" ensureFails (string "123") "12"@@ -74,8 +74,8 @@ ensureFails p "" ensureFails p "a" , testCase "succeed performing the mapping otherwise" do- parse p "4" @?= Success 4- parse p "9" @?= Success 9+ testParse p "4" @?= Success 4+ testParse p "9" @?= Success 9 ] where p = satisfyMap (\c -> digitToInt c <$ guard (isDigit c)) @@ -89,7 +89,7 @@ pureParseWith q "" pureParseWith q "b" impureParseWith q "a"- parse q "a" @?= Success 'a'+ testParse q "a" @?= Success 'a' ensureFails q "" ensureFails q "b" , testCase "parse within a contiguous range" do@@ -97,7 +97,7 @@ pureParseWith r "a" forM_ @[] ['0'..'9'] $ \c -> do impureParseWith r (pure c)- parse r (pure c) @?= Success c+ testParse r (pure c) @?= Success c ensureFails r "a" ensureFails r "\NUL" ensureFails r ":"@@ -107,7 +107,7 @@ pureParseWith s "a" forM_ @[] ['.', ';', ',', ':'] $ \c -> do impureParseWith s (pure c)- parse s (pure c) @?= Success c+ testParse s (pure c) @?= Success c ensureFails s "a" ensureFails s "\NUL" ]@@ -121,13 +121,13 @@ [ testCase "act like item when given no characters" do pureParseWith p "" ensureFails p ""- parse p "a" @?= Success 'a'- parse p "\ACK" @?= Success '\ACK'+ testParse p "a" @?= Success 'a'+ testParse p "\ACK" @?= Success '\ACK' , testCase "accept all but a specific character" do pureParseWith q "" pureParseWith q "a" impureParseWith q "b"- parse q "5" @?= Success '5'+ testParse q "5" @?= Success '5' ensureFails q "" ensureFails q "a" , testCase "parse within a contiguous range" do@@ -136,18 +136,18 @@ forM_ @[] ['0'..'9'] $ \c -> do pureParseWith r (pure c) ensureFails r (pure c)- parse r "a" @?= Success 'a'- parse r "\NUL" @?= Success '\NUL'- parse r ":" @?= Success ':'- parse r "/" @?= Success '/'+ testParse r "a" @?= Success 'a'+ testParse r "\NUL" @?= Success '\NUL'+ testParse r ":" @?= Success ':'+ testParse r "/" @?= Success '/' , testCase "parse any other sets" do pureParseWith s "" impureParseWith s "a" forM_ @[] ['.', ';', ',', ':'] $ \c -> do pureParseWith s (pure c) ensureFails s (pure c)- parse s "a" @?= Success 'a'- parse s "\NUL" @?= Success '\NUL'+ testParse s "a" @?= Success 'a'+ testParse s "\NUL" @?= Success '\NUL' ] where p = noneOf [] q = noneOf ['a']@@ -158,11 +158,11 @@ stringsTests = testGroup "strings should" [ testCase "reject any empty strings" do throws @RequirementUnsatisfied (strings ["abc", "323", ""]) , testCase "have longest match behaviour" do- parse p "hello" @?= Success "hello"- parse p "hell" @?= Success "hell"- parse p "he" @?= Success "h"- parse p "123" @?= Success "123"- parse p "124" @?= Success "1"+ testParse p "hello" @?= Success "hello"+ testParse p "hell" @?= Success "hell"+ testParse p "he" @?= Success "h"+ testParse p "123" @?= Success "123"+ testParse p "124" @?= Success "1" , testCase "reject anything outside of the set" do ensureFails p "543" ensureFails p "good"@@ -173,14 +173,14 @@ trieTests = testGroup "trie should" [ testCase "reject any empty strings" do throws @RequirementUnsatisfied (trie' ["" --> unit]) , testCase "have longest match behaviour" do- parse p "hello" @?= Success "hello"- parse p "hell" @?= Success "hell"- parse p "h" @?= Success "h"- parse p "he" @?= Success "h"- parse p "hi" @?= Success "hi"- parse p "good" @?= Success "good"- parse p "goodby" @?= Success "good"- parse p "goodbye" @?= Success "goodbye"+ testParse p "hello" @?= Success "hello"+ testParse p "hell" @?= Success "hell"+ testParse p "h" @?= Success "h"+ testParse p "he" @?= Success "h"+ testParse p "hi" @?= Success "hi"+ testParse p "good" @?= Success "good"+ testParse p "goodby" @?= Success "good"+ testParse p "goodbye" @?= Success "goodbye" , testCase "reject anything outside of the set" do ensureFails p "543" ensureFails p "god"
test/Text/Gigaparsec/CombinatorTests.hs view
@@ -40,7 +40,7 @@ , testCase "behave like p for [p]" do (choice [char 'a'] ~~ char 'a') ["", "a", "b"] , testCase "parse in order" do- parse (choice [string "a", string "b", string "bc"]) "bcd" @?= Success "b"+ testParse (choice [string "a", string "b", string "bc"]) "bcd" @?= Success "b" , testCase "fail if none of the parsers succeed" do ensureFails (choice [string "a", string "b", string "bc"]) "c" ]@@ -48,9 +48,9 @@ optionTests :: TestTree optionTests = testGroup "option should" [ testCase "succeed with Just if p succeeds" do- parse (option (char 'a')) "a" @?= Success (Just 'a')+ testParse (option (char 'a')) "a" @?= Success (Just 'a') , testCase "succeed with Nothing if p fails withot consumption" do- parse (option (char 'a')) "b" @?= Success Nothing+ testParse (option (char 'a')) "b" @?= Success Nothing , testCase "fail if p fails with consumption" do ensureFails (option (string "ab")) "a" ]@@ -58,7 +58,7 @@ decideTests :: TestTree decideTests = testGroup "decide should" [ testCase "succeed for Just" do- parse (decide (Just <$> char 'a')) "a" @?= Success 'a'+ testParse (decide (Just <$> char 'a')) "a" @?= Success 'a' , testCase "fail for Nothing" do ensureFails @() (decide (pure Nothing)) "" , testCase "compose with option to become identity" do let id' = decide . option@@ -70,17 +70,17 @@ fromMaybeSTests :: TestTree fromMaybeSTests = testGroup "fromMaybeS should" [ testCase "succeed for Just" do- parse (fromMaybeS (pure 'b') (Just <$> char 'a')) "a" @?= Success 'a'+ testParse (fromMaybeS (pure 'b') (Just <$> char 'a')) "a" @?= Success 'a' , testCase "succeed for None" do- parse (fromMaybeS (pure 'b') (Nothing <$ char 'a')) "a" @?= Success 'b'+ testParse (fromMaybeS (pure 'b') (Nothing <$ char 'a')) "a" @?= Success 'b' ] optionalTests :: TestTree optionalTests = testGroup "optional should" [ testCase "succeed if p succeeds" do- parse (optional (char 'a')) "a" @?= Success ()+ testParse (optional (char 'a')) "a" @?= Success () , testCase "also succeed if p fails without consumption" do- parse (optional (char 'a')) "b" @?= Success ()+ testParse (optional (char 'a')) "b" @?= Success () , testCase "fail if p failed with consumption" do ensureFails (optional (string "ab")) "a" ]@@ -89,64 +89,64 @@ manyNTests = testGroup "manyN should" [ testCase "ensure that n are parsed" do forM_ [0..10] \n -> do- parse (manyN n item) (replicate n 'a') @?= Success (replicate n 'a')+ testParse (manyN n item) (replicate n 'a') @?= Success (replicate n 'a') ensureFails (manyN (n + 1) item) (replicate n 'a') , testCase "not care if more are present" do forM_ [0..10] \n ->- parse (manyN n item) (replicate (n + 1) 'a') @?= Success (replicate (n + 1) 'a')+ testParse (manyN n item) (replicate (n + 1) 'a') @?= Success (replicate (n + 1) 'a') ] skipManyNTests :: TestTree skipManyNTests = testGroup "skipManyN should" [ testCase "ensure that n are parsed" do forM_ [0..10] \n -> do- parse (skipManyN n item) (replicate n 'a') @?= Success ()+ testParse (skipManyN n item) (replicate n 'a') @?= Success () ensureFails (skipManyN (n + 1) item) (replicate n 'a') , testCase "not care if more are present" do forM_ [0..10] \n ->- parse (skipManyN n item) (replicate (n + 1) 'a') @?= Success ()+ testParse (skipManyN n item) (replicate (n + 1) 'a') @?= Success () ] sepByTests :: TestTree sepByTests = testGroup "sepBy should" [ testCase "accept empty input" do- parse (sepBy (char 'a') (char 'b')) "" @?= Success []+ testParse (sepBy (char 'a') (char 'b')) "" @?= Success [] , testCase "parse more than 1" do- parse (sepBy (char 'a') (char 'b')) "aba" @?= Success ['a', 'a']+ testParse (sepBy (char 'a') (char 'b')) "aba" @?= Success ['a', 'a'] ] sepBy1Tests :: TestTree sepBy1Tests = testGroup "sepBy1 should" [ testCase "not allow sep at the end of chain" do ensureFails p "ab"- , testCase "be able to parse 2 or more p" do- parse p "aba" @?= Success ['a', 'a']- parse p "ababa" @?= Success ['a', 'a', 'a']- parse p "abababa" @?= Success ['a', 'a', 'a', 'a']+ , testCase "be able to testParse 2 or more p" do+ testParse p "aba" @?= Success ['a', 'a']+ testParse p "ababa" @?= Success ['a', 'a', 'a']+ testParse p "abababa" @?= Success ['a', 'a', 'a', 'a'] , testCase "require a p" do ensureFails p ""- parse p "a" @?= Success ['a']+ testParse p "a" @?= Success ['a'] ] where p = sepBy1 (char 'a') (char 'b') sepEndByTests :: TestTree sepEndByTests = testGroup "sepEndBy should" [ testCase "accept empty input" do- parse (sepEndBy (char 'a') (char 'b')) "" @?= Success []+ testParse (sepEndBy (char 'a') (char 'b')) "" @?= Success [] , testCase "parse more than 1" do- parse (sepEndBy (char 'a') (char 'b')) "aba" @?= Success ['a', 'a']+ testParse (sepEndBy (char 'a') (char 'b')) "aba" @?= Success ['a', 'a'] ] sepEndBy1Tests :: TestTree sepEndBy1Tests = testGroup "sepEndBy1 should" [ testCase "require a p" do ensureFails p ""- , testCase "not require sep at end of chain" do parse p "aa" @?= Success ["aa"]- , testCase "be able to parse 2 or more p" do- parse p "aabbaa" @?= Success ["aa", "aa"]- parse p "aabbaabbaa" @?= Success ["aa", "aa", "aa"]- , testCase "be able to parse a final sep" do- parse p "aabb" @?= Success ["aa"]- parse p "aabbaabb" @?= Success ["aa", "aa"]- parse p "aabbaabbaabb" @?= Success ["aa", "aa", "aa"]+ , testCase "not require sep at end of chain" do testParse p "aa" @?= Success ["aa"]+ , testCase "be able to testParse 2 or more p" do+ testParse p "aabbaa" @?= Success ["aa", "aa"]+ testParse p "aabbaabbaa" @?= Success ["aa", "aa", "aa"]+ , testCase "be able to testParse a final sep" do+ testParse p "aabb" @?= Success ["aa"]+ testParse p "aabbaabb" @?= Success ["aa", "aa"]+ testParse p "aabbaabbaabb" @?= Success ["aa", "aa", "aa"] , testCase "fail if p fails after consuming input" do ensureFails p "aabab" , testCase "fail if sep fails after consuming input" do@@ -157,9 +157,9 @@ endByTests :: TestTree endByTests = testGroup "endBy should" [ testCase "accept empty input" do- parse (endBy (char 'a') (char 'b')) "" @?= Success []+ testParse (endBy (char 'a') (char 'b')) "" @?= Success [] , testCase "parse more than 1" do- parse (endBy (char 'a') (char 'b')) "abab" @?= Success ['a', 'a']+ testParse (endBy (char 'a') (char 'b')) "abab" @?= Success ['a', 'a'] ] endBy1Tests :: TestTree@@ -167,10 +167,10 @@ [ testCase "require a p" do ensureFails p "" , testCase "require a sep at the end of chain" do ensureFails p "aa"- parse p "aabb" @?= Success ["aa"]- , testCase "be able to parse 2 or more p" do- parse p "aabbaabb" @?= Success ["aa", "aa"]- parse p "aabbaabbaabb" @?= Success ["aa", "aa", "aa"]+ testParse p "aabb" @?= Success ["aa"]+ , testCase "be able to testParse 2 or more p" do+ testParse p "aabbaabb" @?= Success ["aa", "aa"]+ testParse p "aabbaabbaabb" @?= Success ["aa", "aa", "aa"] , testCase "fail if p fails after consuming input" do ensureFails p "aaba" ]@@ -180,10 +180,10 @@ manyTillTests = testGroup "manyTill should" [ testCase "require an end" do ensureFails p "aa"- parse p "ab" @?= Success ['a']- , testCase "parse the end without result" do parse p "b" @?= Success []+ testParse p "ab" @?= Success ['a']+ , testCase "parse the end without result" do testParse p "b" @?= Success [] , testCase "parse p until the end is found" do- parse p "aaaaaaaaaab" @?= Success (replicate 10 'a')+ testParse p "aaaaaaaaaab" @?= Success (replicate 10 'a') ensureFails (manyTill (string "aa") (char 'b')) "aaab" ] where p = manyTill (char 'a') (char 'b')@@ -191,7 +191,7 @@ someTillTests :: TestTree someTillTests = testGroup "someTill should" [ testCase "parse at least 1 p" do- parse p "ab" @?= Success ['a']+ testParse p "ab" @?= Success ['a'] ensureFails p "a" ensureFails p "b" ]@@ -200,12 +200,12 @@ countTests :: TestTree countTests = testGroup "count should" [ testCase "report how many successful parses occurred" do- parse p "" @?= Success 0+ testParse p "" @?= Success 0 ensureFails q ""- parse p "ab" @?= Success 1- parse q "ab" @?= Success 1- parse p "ababab" @?= Success 3- parse q "ababab" @?= Success 3+ testParse p "ab" @?= Success 1+ testParse q "ab" @?= Success 1+ testParse p "ababab" @?= Success 3+ testParse q "ababab" @?= Success 3 , testCase "not allow partial results" do ensureFails p "aba" ]@@ -217,9 +217,9 @@ [ testCase "should be pure [] for n <= 0" do (exactly 0 (char 'a') ~~ pure []) ["", "a"] (exactly (-1) (char 'a') ~~ pure []) ["", "a"]- , testCase "should parse n times for n > 0" do+ , testCase "should testParse n times for n > 0" do forM_ [0..100] \n ->- parse (exactly n (char 'a')) (replicate n 'a') @?= Success (replicate n 'a')+ testParse (exactly n (char 'a')) (replicate n 'a') @?= Success (replicate n 'a') , testCase "fail if n inputs are not present" do ensureFails (exactly 2 (char 'a')) "a" ]@@ -228,11 +228,11 @@ rangeTests = testGroup "range should" [ testCase "collect results up instead of count" do ensureFails p "a"- parse p "ab" @?= Success ['a', 'b']- parse p "abc" @?= Success ['a', 'b', 'c']- parse p "abcd" @?= Success ['a', 'b', 'c', 'd']- parse p "abcde" @?= Success ['a', 'b', 'c', 'd', 'e']- parse q "abcdef" @?= Success ['a', 'b', 'c', 'd', 'e']+ testParse p "ab" @?= Success ['a', 'b']+ testParse p "abc" @?= Success ['a', 'b', 'c']+ testParse p "abcd" @?= Success ['a', 'b', 'c', 'd']+ testParse p "abcde" @?= Success ['a', 'b', 'c', 'd', 'e']+ testParse q "abcdef" @?= Success ['a', 'b', 'c', 'd', 'e'] , testCase "should act as pure [] when range is bad" do (range (-1) 3 item ~~ pure []) ["", "a"] (range 2 1 item ~~ pure []) ["", "a"]@@ -244,11 +244,11 @@ range_Tests = testGroup "range_ should" [ testCase "perform a range with no results" do ensureFails p "a"- parse p "ab" @?= Success ()- parse p "abc" @?= Success ()- parse p "abcd" @?= Success ()- parse p "abcde" @?= Success ()- parse q "abcdef" @?= Success ()+ testParse p "ab" @?= Success ()+ testParse p "abc" @?= Success ()+ testParse p "abcd" @?= Success ()+ testParse p "abcde" @?= Success ()+ testParse q "abcdef" @?= Success () , testCase "should act as unit when range is bad" do (range_ (-1) 3 item ~~ unit) ["", "a"] (range_ 2 1 item ~~ unit) ["", "a"]@@ -260,19 +260,19 @@ countRangeTests = testGroup "countRange should" [ testCase "count the parses within the range" do ensureFails p "ab"- parse p "abab" @?= Success 2- parse p "ababab" @?= Success 3- parse p "abababab" @?= Success 4- parse p "ababababab" @?= Success 5- parse p "abababababab" @?= Success 5+ testParse p "abab" @?= Success 2+ testParse p "ababab" @?= Success 3+ testParse p "abababab" @?= Success 4+ testParse p "ababababab" @?= Success 5+ testParse p "abababababab" @?= Success 5 ensureFails p "ababababa" ensureFails q "ab"- parse q "abab" @?= Success 2- parse q "ababab" @?= Success 3- parse q "abababab" @?= Success 4- parse q "ababababab" @?= Success 5- parse q "abababababab" @?= Success 5- parse q "ababababa" @?= Success 4+ testParse q "abab" @?= Success 2+ testParse q "ababab" @?= Success 3+ testParse q "abababab" @?= Success 4+ testParse q "ababababab" @?= Success 5+ testParse q "abababababab" @?= Success 5+ testParse q "ababababa" @?= Success 4 , testCase "should act as unit when range is bad" do (countRange (-1) 3 item ~~ pure 0) ["", "a"] (countRange 2 1 item ~~ pure 0) ["", "a"]
+ test/Text/Gigaparsec/ErrorsTests.hs view
@@ -0,0 +1,384 @@+{-# LANGUAGE OverloadedLists #-}+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}+{-# HLINT ignore "Alternative law, right identity" #-}+module Text.Gigaparsec.ErrorsTests where++import Prelude hiding (fail)++import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.ExpectedFailure++import Text.Gigaparsec+import Text.Gigaparsec.Char+import Text.Gigaparsec.Combinator+import Text.Gigaparsec.Errors.Combinator+import Text.Gigaparsec.Internal.Test+import Text.Gigaparsec.Internal.TestError++import Data.Set qualified as Set (map)++tests :: TestTree+tests = testGroup "Errors" [ labelTests+ , hideTests+ , explainTests+ , emptyTests+ , failTests+ , unexpectedTests+ , lookAheadTests+ , notFollowedByTests+ , amendTests+ , entrenchTests+ , dislodgeTests+ , amendThenDislodgeTests+ , partialAmendTests+ , oneOfTests+ , noneOfTests+ , regressionTests+ ]++labelTests :: TestTree+labelTests = testGroup "label should"+ [ testCase "affect base error messages" do+ testParse (char 'a' <?> ["ay!"]) "b" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "b")) [Named "ay!"] [] 1))+ testParse (char 'a' <?> ["ay!", "see!"]) "b" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "b")) [Named "ay!", Named "see!"] [] 1))+ , testCase "work across a recursion boundary" do+ let r = string "correct error message" <:> r+ let p = r <?> ["nothing but this :)"]+ testParse p "" @?=+ Failure (TestError (1, 1) (VanillaError (Just EndOfInput) [Named "nothing but this :)"] [] 1))+ testParse p "correct error message" @?=+ Failure (TestError (1, 22) (VanillaError (Just EndOfInput) [Raw "correct error message"] [] 1))+ , testCase "replace everything under the label" do+ let s = label ["hi"] (optional (char 'a') *> optional (char 'b')) *> char 'c'+ testParse s "e" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "e")) [Named "hi", Raw "c"] [] 1))+ let t = label ["hi"] (optional (char 'a') *> label ["bee"] (optional (char 'b'))) *> char 'c'+ testParse t "e" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "e")) [Named "hi", Raw "c"] [] 1))+ testParse t "ae" @?=+ Failure (TestError (1, 2) (VanillaError (Just (Raw "e")) [Named "bee", Raw "c"] [] 1))+ let v = label ["hi"] (hide (optional (char 'a')) *> label ["bee"] (optional (char 'b'))) *> char 'c'+ testParse v "e" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "e")) [Named "hi", Raw "c"] [] 1))+ testParse v "ae" @?=+ Failure (TestError (1, 2) (VanillaError (Just (Raw "e")) [Named "bee", Raw "c"] [] 1))+ , testCase "should not replace hints if input is consumed" do+ testParse ((many digit <?> ["number"]) <* eof) "1e" @?=+ Failure (TestError (1, 2) (VanillaError (Just (Raw "e")) [Named "digit", EndOfInput] [] 1))+ ]++hideTests :: TestTree+hideTests = testGroup "hide should"+ [ testCase "not produce any visible output" do+ testParse (hide (char 'a')) "" @?=+ Failure (TestError (1, 1) (VanillaError Nothing [] [] 0))+ testParse (hide (string "a")) "" @?=+ Failure (TestError (1, 1) (VanillaError Nothing [] [] 0))+ testParse (hide digit) "" @?=+ Failure (TestError (1, 1) (VanillaError Nothing [] [] 0))+ , testCase "suppress hints even if input is consumed" do+ testParse (hide (many digit) <* eof) "1e" @?=+ Failure (TestError (1, 2) (VanillaError (Just (Raw "e")) [EndOfInput] [] 1))+ , testCase "not allow hints to be unsuppressed by another label" do+ testParse (label ["hey"] (hide (many digit)) <* eof) "1e" @?=+ Failure (TestError (1, 2) (VanillaError (Just (Raw "e")) [EndOfInput] [] 1))+ ]++emptyTests :: TestTree+emptyTests = testGroup "empty should"+ [ testCase "produce unknown error messages" do+ testParse @() empty "b" @?= Failure (TestError (1, 1) (VanillaError Nothing [] [] 0))+ , testCase "produce no unknown message under influence of label" do+ testParse @() (empty <?> ["something, at least"]) "b" @?=+ Failure (TestError (1, 1) (VanillaError Nothing [Named "something, at least"] [] 0))+ , testCase "not produce an error message at end of <|> chain" do+ testParse (char 'a' <|> empty) "b" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "b")) [Raw "a"] [] 1))+ , testCase "produce an expected error under the influence of label in <|> chain" do+ testParse (char 'a' <|> label ["something, at least"] empty) "b" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "b")) [Raw "a", Named "something, at least"] [] 1))+ , expectFailBecause "no widening for carets in vanilla" $ testCase "have an effect if its caret is wider" do+ testParse (char 'a' <|> emptyWide 3) "bcd" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "bcd")) [Raw "a"] [] 3))+ ]++explainTests :: TestTree+explainTests = testGroup "explain should"+ [ testCase "provide a message but only on failure" do+ testParse @Int (explain "oops!" empty) "" @?=+ Failure (TestError (1, 1) (VanillaError Nothing [] ["oops!"] 0))+ testParse (explain "requires an a" (char 'a')) "b" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "b")) [Raw "a"] ["requires an a"] 1))+ testParse (explain "an a" (char 'a') <|> explain "a b" (char 'b')) "c" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "c")) [Raw "a", Raw "b"] ["an a", "a b"] 1))+ , testCase "not have any effect when more input has been consumed since it was added" do+ testParse (explain "should be absent" (char 'a') *> char 'b') "a" @?=+ Failure (TestError (1, 2) (VanillaError (Just EndOfInput) [Raw "b"] [] 1))+ testParse (explain "should be absent" (char 'a') <|> (char 'b' *> digit)) "b" @?=+ Failure (TestError (1, 2) (VanillaError (Just EndOfInput) [Named "digit"] [] 1))+ ]++failTests :: TestTree+failTests = testGroup "fail should"+ [ testCase "yield a raw message" do+ testParse @Int (fail ["hi"]) "b" @?=+ Failure (TestError (1, 1) (SpecialisedError ["hi"] 1))+ , expectFailBecause "no cross-error width merging" $ testCase "be flexible when the width is unspecified" do+ testParse (string "abc" <|> fail ["hi"]) "xyz" @?=+ Failure (TestError (1, 1) (SpecialisedError ["hi"] 3))+ , testCase "dominate otherwise" do+ testParse (string "abc" <|> failWide 2 ["hi"]) "xyz" @?=+ Failure (TestError (1, 1) (SpecialisedError ["hi"] 2))+ ]++unexpectedTests :: TestTree+unexpectedTests = testGroup "unexpected should"+ [ testCase "yield changes to unexpected messages" do+ testParse @() (unexpected "bee") "b" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Named "bee")) [] [] 1))+ , testCase "produce expected message under influence of label, along with original message" do+ testParse (char 'a' <|> label ["something less cute"] (unexpected "bee")) "b" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Named "bee")) [Raw "a", Named "something less cute"] [] 1))+ , expectFailBecause "no widening for carets in vanilla" $ testCase "be flexible when the width is unspecified" do+ testParse (string "abc" <|> unexpected "bee") "xyz" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Named "bee")) [Raw "abc"] [] 3))+ , testCase "dominate otherwise" do+ testParse (string "abc" <|> unexpectedWide 2 "bee") "xyz" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Named "bee")) [Raw "abc"] [] 2))+ ]++lookAheadTests :: TestTree+lookAheadTests = testGroup "lookAhead should"+ [ testCase "produce no hints following it" do+ let p = char 'a' <|> lookAhead (optional digit *> char 'c') <|> char 'b'+ testParse p "d" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "d")) [Raw "a", Raw "b", Raw "c", Named "digit"] [] 1))+ let q = char 'a' <|> lookAhead (optional digit) *> char 'c' <|> char 'b'+ testParse q "d" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "d")) [Raw "a", Raw "b", Raw "c"] [] 1))+ let r = char 'a' <|> lookAhead digit *> char 'c' <|> char 'b'+ testParse r "d" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "d")) [Raw "a", Raw "b", Named "digit"] [] 1))+ ]++notFollowedByTests :: TestTree+notFollowedByTests = testGroup "notFollowedBy should"+ [ testCase "produce no hints" do+ let p = char 'a' <|> notFollowedBy (optional digit) *> char 'c' <|> char 'b'+ testParse p "d" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "d")) [Raw "a", Raw "b"] [] 1))+ let q = char 'a' <|> notFollowedBy digit *> char 'c' <|> char 'b'+ testParse q "d" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "d")) [Raw "a", Raw "b", Raw "c"] [] 1))+ ]++eofTests :: TestTree+eofTests = testGroup "eof should"+ [ testCase "produce expected end of input" do+ testParse eof "a" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "a")) [EndOfInput] [] 1))+ , testCase "change message under the influence of label" do+ testParse (label ["something more"] eof) "a" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "a")) [Named "something more"] [] 1))+ ]++amendTests :: TestTree+amendTests = testGroup "amend should"+ [ testCase "change error messages under it" do+ let p = char 'a' *> amend (char 'b' *> char 'c' *> char 'd')+ case testParse p "ab" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ case testParse p "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ , testCase "not affect input consumption" do+ ensureFails (amend (char 'a' *> char 'b') <|> char 'a') "a"+ ]++entrenchTests :: TestTree+entrenchTests = testGroup "entrench should"+ [ testCase "prevent the change of error messages under it" do+ let p = char 'a' *> amend (char 'b' *> entrench (char 'c') *> char 'd')+ case testParse p "ab" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse p "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ let q = char 'a' *> amend (char 'b' *> char 'c' *> entrench (char 'd'))+ case testParse q "ab" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ case testParse q "abc" of+ Failure (TestError pos _) -> pos @?= (1, 4)+ _ -> assertFailure "parser must fail"+ , testCase "not prevent the action of amend inside it" do+ let p = char 'a' *> amend (char 'b' *> entrench (amend (char 'c' *> char 'd' *> entrench (char 'e'))) *> char 'f')+ case testParse p "ab" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse p "abc" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse p "abcd" of+ Failure (TestError pos _) -> pos @?= (1, 5)+ _ -> assertFailure "parser must fail"+ case testParse p "abcde" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ ]++dislodgeTests :: TestTree+dislodgeTests = testGroup "dislodge should"+ [ testCase "undo an entrench so that amend works again" do+ let p = char 'a' *> amend (char 'b' *> dislodge (entrench (entrench (char 'c'))) *> char 'd')+ case testParse p "ab" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ case testParse p "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ , testCase "not prevent another entrench from occurring" do+ let p = char 'a' *> amend (char 'b' *> entrench (dislodge (entrench (char 'c'))) *> char 'd')+ case testParse p "ab" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse p "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ , testCase "only unwind as many as instructed if applicable" do+ let p = char 'a' *> amend (char 'b' *> dislodgeBy 1 (entrench (entrench (char 'c'))) *> char 'd')+ let q = char 'a' *> amend (char 'b' *> dislodgeBy 2 (entrench (entrench (char 'c'))) *> char 'd')+ case testParse p "ab" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse p "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ case testParse q "ab" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ case testParse q "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ ]++amendThenDislodgeTests :: TestTree+amendThenDislodgeTests = testGroup "amendThenDislodge should"+ [ testCase "amend only non-entrenched messages and dislodge those that are" do+ let p = char 'a' *> amendThenDislodge (char 'b' *> entrench (entrench (char 'c')) *> char 'd')+ let q = amend p+ case testParse p "ab" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse p "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ case testParse q "ab" of+ Failure (TestError pos _) -> pos @?= (1, 1)+ _ -> assertFailure "parser must fail"+ case testParse q "abc" of+ Failure (TestError pos _) -> pos @?= (1, 1)+ _ -> assertFailure "parser must fail"+ , testCase "only unwind as many as instructed if applicable" do+ let p = char 'a' *> amendThenDislodgeBy 1 (char 'b' *> entrench (entrench (char 'c')) *> char 'd')+ let q = amend p+ let r = char 'a' *> amendThenDislodgeBy 2 (char 'b' *> entrench (entrench (char 'c')) *> char 'd')+ let s = amend r+ case testParse p "ab" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse p "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ case testParse q "ab" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse q "abc" of+ Failure (TestError pos _) -> pos @?= (1, 1)+ _ -> assertFailure "parser must fail"+ case testParse r "ab" of+ Failure (TestError pos _) -> pos @?= (1, 3)+ _ -> assertFailure "parser must fail"+ case testParse r "abc" of+ Failure (TestError pos _) -> pos @?= (1, 2)+ _ -> assertFailure "parser must fail"+ case testParse s "ab" of+ Failure (TestError pos _) -> pos @?= (1, 1)+ _ -> assertFailure "parser must fail"+ case testParse s "abc" of+ Failure (TestError pos _) -> pos @?= (1, 1)+ _ -> assertFailure "parser must fail"+ ]++partialAmendTests :: TestTree+partialAmendTests = testGroup "partialAmend should"+ [ testCaseSteps "perform visual amendment but allow for domination" \step -> do+ let errorMaker n msg = atomic (exactly n (char 'a') *> (char 'b' <|> fail [msg]))++ step "a regular amend should lose against an even shallower error"+ let p = errorMaker 2 "small" <|> amend (errorMaker 3 "big")+ testParse p (replicate 4 'a') @?= Failure (TestError (1, 3) (SpecialisedError ["small"] 1))++ step "a partial amend can win against an error at a lesser offset but greater presentation"+ let q = errorMaker 2 "small" <|> partialAmend (errorMaker 3 "big")+ testParse q (replicate 4 'a') @?= Failure (TestError (1, 1) (SpecialisedError ["big"] 1))++ step "however, they do not win at equal underlying offset"+ let r = errorMaker 3 "first" <|> partialAmend (errorMaker 3 "second")+ testParse r (replicate 4 'a') @?= Failure (TestError (1, 4) (SpecialisedError ["first"] 1))+ ]++--TODO: filter tests++oneOfTests :: TestTree+oneOfTests = testGroup "oneOf should"+ [ testCase "incorporate range notation into the error" do+ testParse (oneOf ['0' .. '9']) "a" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "a")) [Named "one of \"0\" to \"9\""] [] 1))+ , testCase "incorporate sets of characters into error" do+ testParse (oneOf ['0', '2' .. '9']) "a" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "a")) (Set.map (Named . show . (: [])) ['0', '2' .. '9']) [] 1))+ ]++noneOfTests :: TestTree+noneOfTests = testGroup "noneOf should"+ [ testCase "incorporate range notation into the error" do+ testParse (noneOf ['0' .. '9']) "8" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "8")) [Named "anything outside of \"0\" to \"9\""] [] 1))+ , expectFailBecause "no label applied yet" $ testCase "incorporate sets of characters into error" do+ testParse (noneOf ['0', '2' .. '9']) "8" @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "8")) [Named "anything except \"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", or \"9\""] [] 1))+ ]++--TODO: patterns tests++regressionTests :: TestTree+regressionTests = testGroup "thou shalt not regress"+ [ testGroup "hints should"+ [ testCase "incorporate only with errors at the same offset depth" do+ let p = atomic (char 'a' *> digit)+ let parser = optional (char 'b' <?> ["b"]) *> label ["foo"] p+ case testParse parser "aa" of+ Failure (TestError (1, 2) (VanillaError _ expecteds _ 1)) -> do+ expecteds @?= [Named "digit"]+ err -> assertFailure $ "error message " ++ show err ++ " did not match"+ let q = amend (char 'a' *> digit)+ let qarser = optional (char 'b' <?> ["b"]) *> label ["foo"] q+ case testParse qarser "aa" of+ Failure (TestError (1, 1) (VanillaError _ expecteds _ 1)) -> do+ expecteds @?= [Named "foo", Named "b"]+ err -> assertFailure $ "error message " ++ show err ++ " did not match"+ ]+ , testGroup "amend should"+ -- FIXME: unclear why this would be the case+ [ expectFail $ testCase "ensure that errors pick up a new unexpected token" do+ let greeting = string "hello world" <* char '!'+ testParse (amend greeting <?> ["greeting"]) "hello world." @?=+ Failure (TestError (1, 1) (VanillaError (Just (Raw "h")) [Named "greeting"] [] 1))+ ]+ ]
test/Text/Gigaparsec/Expr/ChainTests.hs view
@@ -7,7 +7,7 @@ import Text.Gigaparsec.Char (digit) import Text.Gigaparsec.Expr.Chain -import Text.Gigaparsec.Internal.Test (parseAll, ensureFails)+import Text.Gigaparsec.Internal.Test (testParseAll, ensureFails, testParse) import Text.Gigaparsec.Internal.PlainString () import Data.Char (digitToInt)@@ -27,9 +27,9 @@ postfixTests :: TestTree postfixTests = testGroup "postfix should" [ testCase "require an initial value" do- parseAll (postfix ("1" $> 1) ("+" $> succ)) "1" @?= Success 1+ testParseAll (postfix ("1" $> 1) ("+" $> succ)) "1" @?= Success 1 , testCase "parse all operators that follow" do- parseAll (postfix ("1" $> 1) ("+" $> succ)) "1++++++++++++++" @?= Success 15+ testParseAll (postfix ("1" $> 1) ("+" $> succ)) "1++++++++++++++" @?= Success 15 , testCase "fail if an operator fails after consuming input" do ensureFails (postfix ("1" $> 1) ("++" $> succ)) "1+++++++++++++++" ]@@ -38,9 +38,9 @@ postfix1Tests = testGroup "postfix1 should" [ testCase "require an initial value" do ensureFails (postfix1 id ("1" $> 1) ("+" $> succ)) "1"- parseAll (postfix1 id ("1" $> 1) ("+" $> succ)) "1+" @?= Success 2+ testParseAll (postfix1 id ("1" $> 1) ("+" $> succ)) "1+" @?= Success 2 , testCase "parse all operators that follow" do- parseAll (postfix1 id ("1" $> 1) ("+" $> succ)) "1++++++++++++++" @?= Success 15+ testParseAll (postfix1 id ("1" $> 1) ("+" $> succ)) "1++++++++++++++" @?= Success 15 , testCase "fail if an operator fails after consuming input" do ensureFails (postfix1 id ("1" $> 1) ("++" $> succ)) "1+++++++++++++++" ]@@ -48,9 +48,9 @@ prefixTests :: TestTree prefixTests = testGroup "prefix should" [ testCase "require an initial value" do- parseAll (prefix ("+" $> succ) ("1" $> 1)) "1" @?= Success 1+ testParseAll (prefix ("+" $> succ) ("1" $> 1)) "1" @?= Success 1 , testCase "parse all operators that follow" do- parseAll (prefix ("+" $> succ) ("1" $> 1)) "++++++++++++++1" @?= Success 15+ testParseAll (prefix ("+" $> succ) ("1" $> 1)) "++++++++++++++1" @?= Success 15 , testCase "fail if an operator fails after consuming input" do ensureFails (prefix ("++" $> succ) ("1" $> 1)) "+++++++++++++++1" ]@@ -59,9 +59,9 @@ prefix1Tests = testGroup "prefix1 should" [ testCase "require an initial value" do ensureFails (prefix1 id ("+" $> succ) ("1" $> 1)) "1"- parseAll (prefix1 id ("+" $> succ) ("1" $> 1)) "+1" @?= Success 2+ testParseAll (prefix1 id ("+" $> succ) ("1" $> 1)) "+1" @?= Success 2 , testCase "parse all operators that follow" do- parseAll (prefix1 id ("+" $> succ) ("1" $> 1)) "++++++++++++++1" @?= Success 15+ testParseAll (prefix1 id ("+" $> succ) ("1" $> 1)) "++++++++++++++1" @?= Success 15 , testCase "fail if an operator fails after consuming input" do ensureFails (prefix1 id ("++" $> succ) ("1" $> 1)) "+++++++++++++++1" ]@@ -70,13 +70,13 @@ chainr1Tests = testGroup "chainr1 should" [ testCase "require an initial value" do let p = chainr1 ("11" $> 1) ("+" $> (+))- parseAll p "11" @?= Success 1+ testParseAll p "11" @?= Success 1 ensureFails p "1" ensureFails p "2" , testCase "parse all operators and values that follow" do- parseAll (chainr1 ("11" $> 1) ("+" $> (+))) "11+11+11+11+11" @?= Success 5+ testParseAll (chainr1 ("11" $> 1) ("+" $> (+))) "11+11+11+11+11" @?= Success 5 , testCase "apply the functions with the correct associativity" do- parseAll (chainr1 (digitToInt <$> digit) ("%" $> mod)) "6%5%2%7" @?= Success 0+ testParseAll (chainr1 (digitToInt <$> digit) ("%" $> mod)) "6%5%2%7" @?= Success 0 , testCase "fail if an operator or p fails after consuming input" do let p = chainr1 ("11" $> 1) ("++" $> (+)) ensureFails p "11+11+11+11+11"@@ -87,7 +87,7 @@ chainrTests = testGroup "chainr should" [ testCase "allow for no initial value" do let p = chainr ("11" $> 1) ("+" $> (+)) 0- parseAll p "" @?= Success 0+ testParseAll p "" @?= Success 0 ensureFails p "1" ] @@ -95,13 +95,13 @@ chainl1Tests = testGroup "chainl1 should" [ testCase "require an initial value" do let p = chainl1 ("11" $> 1) ("+" $> (+))- parseAll p "11" @?= Success 1+ testParseAll p "11" @?= Success 1 ensureFails p "1" ensureFails p "2" , testCase "parse all operators and values that follow" do- parseAll (chainl1 ("11" $> 1) ("+" $> (+))) "11+11+11+11+11" @?= Success 5+ testParseAll (chainl1 ("11" $> 1) ("+" $> (+))) "11+11+11+11+11" @?= Success 5 , testCase "apply the functions with the correct associativity" do- parseAll (chainl1 (digitToInt <$> digit) ("%" $> mod)) "6%5%2%7" @?= Success 1+ testParseAll (chainl1 (digitToInt <$> digit) ("%" $> mod)) "6%5%2%7" @?= Success 1 , testCase "fail if an operator or p fails after consuming input" do let p = chainl1 ("11" $> 1) ("++" $> (+)) ensureFails p "11+11+11+11+11"@@ -112,7 +112,7 @@ chainlTests = testGroup "chainl should" [ testCase "allow for no initial value" do let p = chainl ("11" $> 1) ("+" $> (+)) 0- parseAll p "" @?= Success 0+ testParseAll p "" @?= Success 0 ensureFails p "1"- parse p "2" @?= Success 0+ testParse p "2" @?= Success 0 ]
test/Text/Gigaparsec/Expr/InfixTests.hs view
@@ -6,7 +6,7 @@ import Text.Gigaparsec import Text.Gigaparsec.Expr.Infix -import Text.Gigaparsec.Internal.Test (parseAll)+import Text.Gigaparsec.Internal.Test (testParseAll) import Text.Gigaparsec.Internal.PlainString () data Expr = Add Int Expr | Sub Expr Int | Num Int deriving stock (Eq, Show)@@ -21,14 +21,14 @@ infixr1Tests = testGroup "infixr1 should" [ testCase "correctly accept the use of a wrapping function" do let p = infixr1 Num ("1" $> 1) ("+" $> Add)- parseAll p "1+1+1" @?= Success (Add 1 (Add 1 (Num 1)))- parseAll p "1" @?= Success (Num 1)+ testParseAll p "1+1+1" @?= Success (Add 1 (Add 1 (Num 1)))+ testParseAll p "1" @?= Success (Num 1) ] infixl1Tests :: TestTree infixl1Tests = testGroup "infixl1 should" [ testCase "correctly accept the use of a wrapping function" do let p = infixl1 Num ("1" $> 1) ("-" $> Sub)- parseAll p "1-1-1" @?= Success (Sub (Sub (Num 1) 1) 1)- parseAll p "1" @?= Success (Num 1)+ testParseAll p "1-1-1" @?= Success (Sub (Sub (Num 1) 1) 1)+ testParseAll p "1" @?= Success (Num 1) ]
test/Text/Gigaparsec/ExprTests.hs view
@@ -9,7 +9,7 @@ import Text.Gigaparsec.Expr import Text.Gigaparsec.Expr.Subtype -import Text.Gigaparsec.Internal.Test (parseAll)+import Text.Gigaparsec.Internal.Test (testParseAll) import Text.Gigaparsec.Internal.PlainString () import Data.Char (digitToInt)@@ -46,47 +46,47 @@ [ testCase "result in correct precedence" do let expr = precedence' (digitToInt <$> digit) [ ops InfixL ["*" $> (*)] , ops InfixL ["+" $> (+)]]- parseAll expr "1+2*3+4" @?= Success 11- parseAll expr "1*2+3*4" @?= Success 14+ testParseAll expr "1+2*3+4" @?= Success 11+ testParseAll expr "1*2+3*4" @?= Success 14 , testCase "work for multiple operators at the same level" do let expr = precedence' (digitToInt <$> digit) [ops InfixL ["+" $> (+), "-" $> (-)]]- parseAll expr "1+2-3+4" @?= Success 4- parseAll expr "1-2+3-4" @?= Success (-2)+ testParseAll expr "1+2-3+4" @?= Success 4+ testParseAll expr "1-2+3-4" @?= Success (-2) , testCase "work for mixed associativity operators" do let expr = precedence' (digitToInt <$> digit) [ ops InfixL ["*" $> (*)] , ops InfixR ["+" $> (+)]]- parseAll expr "1+2*3+4" @?= Success 11- parseAll expr "1*2+3*4" @?= Success 14+ testParseAll expr "1+2*3+4" @?= Success 11+ testParseAll expr "1*2+3*4" @?= Success 14 , testCase "parse mathematical expressions" do let expr = precedence' (digitToInt <$> digit <|> "(" *> expr <* ")") [ ops Prefix ["-" $> negate] , ops InfixL ["/" $> div] , ops InfixR ["*" $> (*)] , ops InfixL ["+" $> (+), "-" $> (-)]]- parseAll expr "(2+3)*8" @?= Success 40- parseAll expr "-3+4" @?= Success 1- parseAll expr "-(3+4)" @?= Success (-7)- parseAll expr "(3+-7)*(-2--4)/2" @?= Success (-4)+ testParseAll expr "(2+3)*8" @?= Success 40+ testParseAll expr "-3+4" @?= Success 1+ testParseAll expr "-(3+4)" @?= Success (-7)+ testParseAll expr "(3+-7)*(-2--4)/2" @?= Success (-4) , testCase "parse prefix operators mixed with infix operators" do let expr = precedence' (digitToInt <$> digit <|> "(" *> expr <* ")") [ ops Prefix ["-" $> negate] , ops InfixL ["-" $> (-)]]- parseAll expr "-1" @?= Success (-1)- parseAll expr "2-1" @?= Success 1- parseAll expr "-2-1" @?= Success (-3)- parseAll expr "-(2-1)" @?= Success (-1)- parseAll expr "(-0)-1" @?= Success (-1)+ testParseAll expr "-1" @?= Success (-1)+ testParseAll expr "2-1" @?= Success 1+ testParseAll expr "-2-1" @?= Success (-3)+ testParseAll expr "-(2-1)" @?= Success (-1)+ testParseAll expr "(-0)-1" @?= Success (-1) , testCase "be able to parse prefix operators weaker than an infix" do let expr = precedence' ("." $> Unit) [ ops InfixL [";" $> Bin] , ops Prefix ["~" $> Un]]- parseAll expr "~.;." @?= Success (Un (Bin Unit Unit))+ testParseAll expr "~.;." @?= Success (Un (Bin Unit Unit)) , testCase "generalise to sub-typed structures" do let expr = precedence $ sops InfixN ["<" $> Less] +< sops InfixL ["+" $> Add] +< sops InfixR ["*" $> Mul] +< sops Prefix ["-" $> Neg] +< Atom (Num . digitToInt <$> digit <|> "(" *> (Parens <$> expr) <* ")")- parseAll expr "(7+8)*2+3+6*2" @?=+ testParseAll expr "(7+8)*2+3+6*2" @?= Success (upcast (Add (Add (upcast (Mul (upcast (Parens (upcast (Add (upcast (Num 7)) (upcast (Num 8)))))) (upcast (Num 2))))@@ -98,7 +98,7 @@ +< gops InfixR OfFactor ["*" $> Mul] +< gops Prefix OfAtom ["-" $> Neg] +< Atom (Num . digitToInt <$> digit <|> "(" *> (Parens <$> expr) <* ")")- parseAll expr "(7+8)*2+3+6*2<4" @?=+ testParseAll expr "(7+8)*2+3+6*2<4" @?= Success (Less (Add (Add (upcast (Mul (upcast (Parens (upcast (Add (upcast (Num 7)) (upcast (Num 8)))))) (upcast (Num 2))))@@ -111,7 +111,7 @@ >+ gops InfixL OfFactor ["*" $> Mul'] >+ gops InfixL OfTerm ["+" $> Add] >+ gops InfixN OfExpr ["<" $> Less]- parseAll expr "1*(2+3)" @?=+ testParseAll expr "1*(2+3)" @?= Success (upcast (Mul' (upcast (Num 1)) (upcast (Parens (upcast (Add (upcast (Num 2)) (upcast (Num 3))))))))
test/Text/Gigaparsec/Internal/Test.hs view
@@ -1,9 +1,13 @@ -- A collection of test helpers-{-# LANGUAGE StandaloneDeriving, AllowAmbiguousTypes #-}+{-# LANGUAGE AllowAmbiguousTypes, RecordWildCards #-}+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}+{-# HLINT ignore "Use newtype instead of data" #-} module Text.Gigaparsec.Internal.Test where import Test.Tasty.HUnit +import Text.Gigaparsec.Internal.TestError+ import Text.Gigaparsec import Text.Gigaparsec.Internal import Text.Gigaparsec.Internal.RT@@ -12,28 +16,29 @@ import Control.Monad (unless, forM_) import Type.Reflection (typeOf, typeRep) --- don't @ me-deriving stock instance Eq State-deriving stock instance Show State+data LiftedState = Lifted State -parseAll :: Parsec a -> String -> Result a-parseAll p inp = parse (p <* eof) inp+testParse :: Parsec a -> String -> Result TestError a+testParse = parse @TestError +testParseAll :: Parsec a -> String -> Result TestError a+testParseAll p = testParse (p <* eof)+ -- TODO: could we use quick-check to generate states? -- | Tests to ensure that running the parser on the given string does nothing to the state pureParseWith :: HasCallStack => Parsec a -> String -> Assertion pureParseWith (Parsec p) inp = do run initSt- run (initSt { consumed = True })+ run (initSt { consumed = 1 }) run (initSt { line = 10, col = 20 })- run (initSt { consumed = True, line = 10, col = 20 })+ run (initSt { consumed = 200, line = 10, col = 20 }) where initSt = emptyState inp run :: State -> Assertion run st = do- let st' = runRT (p st (\ !_ -> return) return)- unless (st == st') $+ let st' = runRT (p st (\ !_ s -> return (Lifted s)) (\ _ s -> return (Lifted s)))+ unless (Lifted st == st') $ assertFailure ("expected no change to internal state\n"- ++ "initial state: " ++ show st ++ "\n became: " ++ show st')+ ++ "initial state: " ++ show (Lifted st) ++ "\n became: " ++ show st') -- TODO: could we use quick-check to generate inputs? -- | Tests to ensure that running the parser does nothing to the state@@ -55,7 +60,7 @@ run :: State -> Assertion run st = do let st' = parseState p st- assertBool (show st ++ " should be altered") (st' /= st)+ assertBool (show (Lifted st) ++ " should be altered") (st' /= Lifted st) -- TODO: could we use quick-check to generate inputs? -- | Tests to ensure that running the parser does something to the state@@ -66,10 +71,10 @@ impureParseWith p ":@279" consume :: a -> Parsec a-consume x = Parsec $ \st good _ -> good x (st { consumed = True})+consume x = Parsec $ \st good _ -> good x (st { consumed = consumed st + 1}) ensureFails :: (Show a, HasCallStack) => Parsec a -> String -> Assertion-ensureFails p inp = case parse p inp of+ensureFails p inp = case testParse p inp of Failure{} -> return () Success x -> assertFailure ("parser must fail, but produced: " ++ show x) @@ -91,9 +96,33 @@ qSt = parseState q st unless (pSt == qSt) $ assertFailure ("expected both parsers have the same effect on the state"- ++ "\ninitial state: " ++ show st+ ++ "\ninitial state: " ++ show (Lifted st) ++ "\n got: " ++ show pSt ++ "\n expected: " ++ show qSt) -parseState :: Parsec a -> State -> State-parseState (Parsec p) st = runRT (p st (\ !_ -> return) return)+parseState :: Parsec a -> State -> LiftedState+parseState (Parsec p) st = runRT (p st (\ !_ st' -> return (Lifted st')) (\ _ st' -> return (Lifted st')))++-- don't @ me+instance Eq LiftedState where+ (==) :: LiftedState -> LiftedState -> Bool+ Lifted (State input1 consumed1 line1 col1 _hintValidOffset1 _hints1) ==+ Lifted (State input2 consumed2 line2 col2 _hintValidOffset2 _hints2) =+ consumed1 == consumed2 && line1 == line2 && col1 == col2 && input1 == input2+ -- this throws off a whole bunch of tests, understandably+ -- && hintValidOffset1 == hintValidOffset2 && hints1 == hints2+instance Show LiftedState where+ showsPrec :: Int -> LiftedState -> ShowS+ showsPrec p (Lifted State{..}) = showParen (p > 10) $ showString "State { input = "+ . shows input+ . showString ", consumed = "+ . shows consumed+ . showString ", line = "+ . shows line+ . showString ", col = "+ . shows col+ . showString ", hintsValidOffset = "+ . shows hintsValidOffset+ . showString ", hints = "+ . shows hints+ . showChar '}'
+ test/Text/Gigaparsec/Internal/TestError.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE TypeFamilies #-}+module Text.Gigaparsec.Internal.TestError (+ TestError(..), TestErrorLines(..), TestErrorItem(..)+ ) where++import Text.Gigaparsec.Errors.ErrorBuilder hiding (Token(..))+import Text.Gigaparsec.Errors.ErrorBuilder qualified as Token++import Data.Set (Set)+import Data.List.NonEmpty (NonEmpty)+import Data.Set qualified as Set (fromList)+import Data.List.NonEmpty qualified as NonEmpty (take)++data TestError = TestError !(Word, Word) !TestErrorLines deriving stock (Eq, Show, Ord)+data TestErrorLines = VanillaError !(Maybe TestErrorItem) !(Set TestErrorItem) !(Set String) !Word+ | SpecialisedError !(Set String) !Word deriving stock (Eq, Show, Ord)+data TestErrorItem = Raw !String | Named !String | EndOfInput deriving stock (Eq, Show, Ord)++instance ErrorBuilder TestError where+ format p _ = TestError p++ type Position TestError = (Word, Word)+ pos = (,)++ type Source TestError = ()+ source = const ()++ type ErrorInfoLines TestError = TestErrorLines+ vanillaError = VanillaError+ specialisedError = SpecialisedError++ type ExpectedItems TestError = Set TestErrorItem+ combineExpectedItems = id++ type Messages TestError = Set String+ combineMessages = Set.fromList++ type UnexpectedLine TestError = Maybe TestErrorItem+ unexpected = id+ type ExpectedLine TestError = Set TestErrorItem+ expected = id++ type Message TestError = String+ reason = id+ message = id++ type LineInfo TestError = Word+ lineInfo _ _ _ _ width = width++ numLinesBefore = 2+ numLinesAfter = 2++ type Item TestError = TestErrorItem+ raw = Raw+ named = Named+ endOfInput = EndOfInput++ unexpectedToken :: NonEmpty Char -> Word -> Bool -> Token.Token+ unexpectedToken cs demanded _ = Token.Raw (NonEmpty.take (fromIntegral demanded) cs)
test/Text/Gigaparsec/PrimitiveTests.hs view
@@ -33,14 +33,14 @@ eofTests :: TestTree eofTests = testGroup "eof should" [ testCase "fail if input available" do ensureFails eof "a"- , testCase "succeed if input ended" do parse eof "" @?= Success ()+ , testCase "succeed if input ended" do testParse eof "" @?= Success () , testCase "be pure" do pureParse eof ] pureTests :: TestTree pureTests = testGroup "pure should" [ testCase "be pure" do pureParse unit- , testCase "produce the given result" do parse unit "" @?= Success ()+ , testCase "produce the given result" do testParse unit "" @?= Success () ] emptyTests :: TestTree@@ -71,8 +71,8 @@ , testCase "be impure if the left-hand side is impure and succeeds" do impureParse (consume () <|> empty) , testCase "succeed if the left-hand side succeeds" do- parse (unit <|> empty) "" @?= Success ()- parse (consume () <|> empty) "" @?= Success ()+ testParse (unit <|> empty) "" @?= Success ()+ testParse (consume () <|> empty) "" @?= Success () , testCase "be pure if the right-hand side succeeds purely" do pureParse (empty <|> unit) , testCase "be impure if the right-hand side succeeds impurely" do@@ -95,8 +95,8 @@ , testCase "be pure if the argument fails, even if impure" do pureParse (atomic (consume () <**> empty)) , testCase "not alter failure characteristics of argument" do- parse (atomic (consume ())) "" @?= Success ()- parse (atomic unit) "" @?= Success ()+ testParse (atomic (consume ())) "" @?= Success ()+ testParse (atomic unit) "" @?= Success () ensureFails @Void (atomic empty) "" ensureFails (atomic (consume () <* empty)) "" ]@@ -112,8 +112,8 @@ , testCase "be impure if the argument is impure and fails" do impureParse (lookAhead (consume () <* empty)) , testCase "not alter failure characteristics of argument" do- parse (lookAhead (pure 7)) "" @?= Success 7- parse (lookAhead (consume 14)) "" @?= Success 14+ testParse (lookAhead (pure 7)) "" @?= Success 7+ testParse (lookAhead (consume 14)) "" @?= Success 14 ensureFails @Void (lookAhead empty) "" ensureFails (lookAhead (consume () <* empty)) "" ]@@ -127,8 +127,8 @@ pureParse (notFollowedBy (consume ())) pureParse (notFollowedBy (consume () *> empty)) , testCase "should succeed if the argument fails" do- parse (notFollowedBy empty) "" @?= Success ()- parse (notFollowedBy (consume () *> empty)) "" @?= Success ()+ testParse (notFollowedBy empty) "" @?= Success ()+ testParse (notFollowedBy (consume () *> empty)) "" @?= Success () , testCase "should fail if the argument succeeds" do ensureFails (notFollowedBy unit) "" ensureFails (notFollowedBy (consume ())) ""