Skip to content

Commit 92a5df0

Browse files
Always propagate errors, select deepest errors on failure
1 parent 44f6307 commit 92a5df0

4 files changed

Lines changed: 100 additions & 39 deletions

File tree

src/KDL/Decoder/Arrow.hs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,9 +1062,13 @@ null = valueDataDecoderPrim (SchemaOne NullSchema) $ \case
10621062

10631063
-- | Return the first result that succeeds.
10641064
--
1065-
-- > oneOf [a, b, c] === a <|> b <|> c <|> empty
1065+
-- > oneOf [a, b, c] === a <|> b <|> c
10661066
oneOf :: (Alternative f) => [f a] -> f a
1067-
oneOf = foldr (<|>) empty
1067+
oneOf ms =
1068+
-- Avoid 'empty' if possible
1069+
case NonEmpty.nonEmpty ms of
1070+
Just ms' -> foldr1 (<|>) ms'
1071+
Nothing -> empty
10681072

10691073
-- | Return the given default value if the given action fails.
10701074
--

src/KDL/Decoder/Internal/DecodeM.hs

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,58 +18,112 @@ module KDL.Decoder.Internal.DecodeM (
1818
) where
1919

2020
import Control.Applicative (Alternative (..))
21+
import Data.Bifunctor (first)
22+
import Data.List.NonEmpty (NonEmpty (..))
23+
import Data.List.NonEmpty qualified as NonEmpty
2124
import Data.Text (Text)
2225
import KDL.Decoder.Internal.Error
2326

2427
-- | The monad that returns either a 'DecodeError' or a result of type @a@.
2528
--
26-
-- To a first approximation, this monad is equivalent to the @Either DecodeError@
27-
-- monad, with the following changes:
29+
-- The odd structure here is because of our backtracking semantics. We want to
30+
-- collect all errors that may appear (even if a value is successfully parsed)
31+
-- so that if we get a failure later on, we can return the deepest error, even
32+
-- if it was in a successful branch.
2833
--
29-
-- * Uses continuation-passing style for performance
30-
-- * Collects as many errors as possible, within an Applicative context
34+
-- Take this motivating example: a node takes an arbitrary number of string
35+
-- args. If you pass some strings then a number, it'll successfully parse up to
36+
-- the number and return success, only for the node to fail later with
37+
-- "unexpected argument: 123". But the true error was
38+
-- "unexpected number, expected string".
3139
data DecodeM a
32-
= DecodeM
33-
( forall r.
34-
(DecodeError -> r) ->
35-
(a -> r) ->
36-
r
37-
)
40+
= DecodeM_Found a [BaseDecodeError]
41+
| DecodeM_Fail (NonEmpty BaseDecodeError)
3842

3943
instance Functor DecodeM where
40-
fmap f (DecodeM k) = DecodeM $ \onFail onSuccess -> k onFail (onSuccess . f)
44+
fmap f = \case
45+
DecodeM_Found a es -> DecodeM_Found (f a) es
46+
DecodeM_Fail es -> DecodeM_Fail es
4147
instance Applicative DecodeM where
42-
pure x = DecodeM $ \_ onSuccess -> onSuccess x
43-
DecodeM kf <*> DecodeM ka = DecodeM $ \onFail onSuccess ->
44-
-- Collect all errors
45-
kf
46-
(\e1 -> ka (\e2 -> onFail $ e1 <> e2) (\_ -> onFail e1))
47-
(\f -> ka onFail (onSuccess . f))
48+
pure x = DecodeM_Found x []
49+
l <*> r =
50+
case (l, r) of
51+
(DecodeM_Found f es1, DecodeM_Found a es2) -> DecodeM_Found (f a) (mergeErrorsLR es1 es2)
52+
(DecodeM_Found _ es1, DecodeM_Fail es2) -> DecodeM_Fail (mergeErrorsL es1 es2)
53+
(DecodeM_Fail es1, DecodeM_Found _ es2) -> DecodeM_Fail (mergeErrorsR es1 es2)
54+
(DecodeM_Fail es1, DecodeM_Fail es2) -> DecodeM_Fail (mergeErrors es1 es2)
4855
instance Monad DecodeM where
4956
(>>) = (*>)
50-
DecodeM ka >>= k = DecodeM $ \onFail onSuccess ->
51-
ka onFail $ \a -> let DecodeM kb = k a in kb onFail onSuccess
57+
m >>= k =
58+
case m of
59+
DecodeM_Fail es1 -> DecodeM_Fail es1
60+
DecodeM_Found a es1 ->
61+
case k a of
62+
DecodeM_Found b es2 -> DecodeM_Found b (mergeErrorsLR es1 es2)
63+
DecodeM_Fail es2 -> DecodeM_Fail (mergeErrorsL es1 es2)
5264
instance Alternative DecodeM where
53-
empty = DecodeM $ \onFail _ -> onFail mempty
54-
DecodeM k1 <|> DecodeM k2 = DecodeM $ \onFail onSuccess ->
55-
k1
56-
(\e1 -> k2 (\e2 -> onFail $ e1 <> e2) onSuccess)
57-
onSuccess
65+
empty = failM "<empty>"
66+
l <|> r =
67+
case l of
68+
DecodeM_Found a es1 -> DecodeM_Found a es1
69+
DecodeM_Fail es1 ->
70+
case r of
71+
DecodeM_Found a es2 -> DecodeM_Found a (NonEmpty.toList $ mergeErrorsR es1 es2)
72+
DecodeM_Fail es2 -> DecodeM_Fail (mergeErrors es1 es2)
5873

59-
-- | Run a 'DecodeM' action and return the result or the error.
74+
-- | Run a 'DecodeM' action and return the result or the deepest error found.
6075
runDecodeM :: DecodeM a -> Either DecodeError a
61-
runDecodeM (DecodeM f) = f Left Right
76+
runDecodeM = \case
77+
DecodeM_Found a _ -> Right a
78+
DecodeM_Fail errors -> Left DecodeError{filepath = Nothing, errors}
79+
80+
mergeErrors ::
81+
NonEmpty BaseDecodeError ->
82+
NonEmpty BaseDecodeError ->
83+
NonEmpty BaseDecodeError
84+
mergeErrors es1 es2 =
85+
case compare (key es1) (key es2) of
86+
LT -> es2
87+
EQ -> es1 <> es2
88+
GT -> es1
89+
where
90+
key = length . fst . NonEmpty.head
91+
92+
mergeErrorsL ::
93+
[BaseDecodeError] ->
94+
NonEmpty BaseDecodeError ->
95+
NonEmpty BaseDecodeError
96+
mergeErrorsL l r = maybe r (\l' -> mergeErrors l' r) (NonEmpty.nonEmpty l)
97+
98+
mergeErrorsR ::
99+
NonEmpty BaseDecodeError ->
100+
[BaseDecodeError] ->
101+
NonEmpty BaseDecodeError
102+
mergeErrorsR l r = maybe l (\r' -> mergeErrors l r') (NonEmpty.nonEmpty r)
103+
104+
mergeErrorsLR ::
105+
[BaseDecodeError] ->
106+
[BaseDecodeError] ->
107+
[BaseDecodeError]
108+
mergeErrorsLR l r =
109+
case (l, r) of
110+
([], _) -> r
111+
(_, []) -> l
112+
(x : xs, y : ys) -> NonEmpty.toList $ mergeErrors (x :| xs) (y :| ys)
113+
114+
mapErrors :: (BaseDecodeError -> BaseDecodeError) -> DecodeM a -> DecodeM a
115+
mapErrors f = \case
116+
DecodeM_Found a es -> DecodeM_Found a (fmap f es)
117+
DecodeM_Fail es -> DecodeM_Fail (fmap f es)
62118

63119
-- | Throw an error.
64120
decodeThrow :: DecodeErrorKind -> DecodeM a
65-
decodeThrow e = DecodeM $ \onFail _ -> onFail $ DecodeError Nothing [([], e)]
121+
decodeThrow e = DecodeM_Fail . NonEmpty.singleton $ ([], e)
66122

67123
-- | Throw a 'DecodeError_Custom' error.
68124
failM :: Text -> DecodeM a
69125
failM = decodeThrow . DecodeError_Custom
70126

71127
-- | Add context to all errors that occur in the given action.
72128
addContext :: ContextItem -> DecodeM a -> DecodeM a
73-
addContext ctxItem (DecodeM f) = DecodeM $ \onFail onSuccess -> f (onFail . addCtx) onSuccess
74-
where
75-
addCtx e = e{errors = [(ctxItem : ctx, msg) | (ctx, msg) <- e.errors]}
129+
addContext ctxItem = mapErrors (first (ctxItem :))

src/KDL/Decoder/Internal/Error.hs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ module KDL.Decoder.Internal.Error (
1414
renderDecodeError,
1515
) where
1616

17-
import Control.Applicative ((<|>))
17+
import Data.List.NonEmpty (NonEmpty)
18+
import Data.List.NonEmpty qualified as NonEmpty
1819
import Data.Map qualified as Map
1920
import Data.Text (Text)
2021
import Data.Text qualified as Text
@@ -29,13 +30,9 @@ import KDL.Types (
2930

3031
data DecodeError = DecodeError
3132
{ filepath :: Maybe FilePath
32-
, errors :: [BaseDecodeError]
33+
, errors :: NonEmpty BaseDecodeError
3334
}
3435
deriving (Show, Eq)
35-
instance Semigroup DecodeError where
36-
DecodeError fp1 e1 <> DecodeError fp2 e2 = DecodeError (fp1 <|> fp2) (e1 <> e2)
37-
instance Monoid DecodeError where
38-
mempty = DecodeError Nothing []
3936

4037
type BaseDecodeError = (Context, DecodeErrorKind)
4138
type Context = [ContextItem]
@@ -74,7 +71,11 @@ renderDecodeError decodeError =
7471
$ decodeError.errors
7572
where
7673
-- Group errors with the same contexts together
77-
groupCtxErrors es = Map.toAscList $ Map.fromListWith (<>) [(ctx, [e]) | (ctx, e) <- es]
74+
groupCtxErrors es =
75+
Map.toAscList . Map.fromListWith (<>) $
76+
[ (ctx, [e])
77+
| (ctx, e) <- NonEmpty.toList es
78+
]
7879

7980
addPath =
8081
case decodeError.filepath of

test/KDL/Decoder/SharedSpec/Template.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ apiSpec = do
474474
`shouldSatisfy` decodeErrorMsg
475475
[ "At: foo #0"
476476
, " Unexpected node: bar #0"
477+
, " Expected another node: -"
477478
]
478479

479480
it "fails if any child fails to parse" $ do
@@ -569,6 +570,7 @@ apiSpec = do
569570
`shouldSatisfy` decodeErrorMsg
570571
[ "At: foo #0"
571572
, " Unexpected node: bar #0"
573+
, " Expected another node: -"
572574
]
573575

574576
-- Most behaviors tested with `dashNodesAt`

0 commit comments

Comments
 (0)