morley-1.16.3: src/Morley/Util/MismatchError.hs
-- SPDX-FileCopyrightText: 2022 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA
{- | Helper utilities for rendering mismatch-type errors.
For cases where it's useful to get a diff-style mismatch report, i.e. where
types can be rendered multi-line, a few override instances are needed.
The diff is produced by printing the types in a multiline format, then computing
the line diff with the standard algorithm (as implemented by the @Diff@
package).
In general, assuming the type in question is @T@, those instances
can look like this:
@
instance Buildable (MismatchError T) where
build = buildRenderDocExtended
instance RenderDoc (MismatchError T) where
renderDoc ctx = renderDocDiff ctx . fmap Prettier
@
The @fmap Prettier@ part is only required to enable multi-line rendering.
Additionally, if there's a need to show mismatch errors for lists, the following
instance can be used:
@
instance RenderDoc (MismatchError [T]) where
renderDoc ctx = renderDocDiffList ctx . (fmap . fmap) Prettier
@
-}
module Morley.Util.MismatchError
( MismatchError(..)
, renderDocDiff
, renderDocDiffList
) where
import Prelude hiding (First, (<$>))
import Data.Algorithm.Diff (PolyDiff(..), getDiffBy)
import Data.Text.Lazy as LT (lines, strip)
import Fmt (Buildable(..), nameF)
import Text.PrettyPrint.Leijen.Text (Doc, align, fill, indent, text, vcat, (<$>), (<+>))
import Morley.Michelson.Printer.Util
-- | A helper record datatype representing a mismatch between two values of
-- some type. One is assumed to be in some sense the "expected" value, the other
-- one is assumed to be the "actual" value.
data MismatchError a = MkMismatchError
{ meExpected :: a -- ^ Expected value
, meActual :: a -- ^ Actual value
} deriving stock (Show, Eq, Generic, Functor, Foldable)
deriving anyclass (NFData, Container)
instance {-# OVERLAPPABLE #-} Buildable a => Buildable (MismatchError a) where
build MkMismatchError{..} =
nameF "Expected" (build meExpected) <> nameF "Actual" (build meActual)
instance {-# OVERLAPPABLE #-} RenderDoc a => RenderDoc (MismatchError a) where
renderDoc = renderDocStandard
renderDocStandard :: RenderDoc a => RenderContext -> MismatchError a -> Doc
renderDocStandard _ (fmap (align . renderDoc doesntNeedParens) -> MkMismatchError{..})
= fill 9 "Expected:" <+> meExpected
<$> fill 9 "Actual:" <+> meActual
renderDocStandardList :: RenderDoc a => RenderContext -> MismatchError [a] -> Doc
renderDocStandardList _ (fmap (align . renderDocList doesntNeedParens) -> MkMismatchError{..})
= fill 9 "Expected:" <+> meExpected
<$> fill 9 "Actual:" <+> meActual
-- | Render a mismatch error with a diff.
--
-- This is intended to be used with types for which 'RenderDoc' outputs
-- multiline 'Doc'. Generally those are types wrapped in 'Prettier'.
renderDocDiff :: RenderDoc a => RenderContext -> MismatchError a -> Doc
renderDocDiff ctx err = renderDocStandard ctx err
<$> if expectedAndActualBothSingleLine
then mempty
else "Mismatch:"
<$> indent 2 "--- expected +++ actual"
<$> align (diff errText)
where
errText = fmap (renderDoc doesntNeedParens) err
expectedAndActualBothSingleLine = all (null . drop 1 . LT.lines . printDoc False) errText
-- | Render a mismatch error of lists with a diff.
--
-- This is intended to be used with types for which 'RenderDoc' outputs
-- multiline 'Doc'. Generally those are types wrapped in 'Prettier'.
renderDocDiffList :: RenderDoc a => RenderContext -> MismatchError [a] -> Doc
renderDocDiffList ctx err = renderDocStandardList ctx err
<$> if simple
then mempty
else "Mismatch:"
<$> indent 2 "--- expected +++ actual"
<$> align (diff errText)
where
errText = fmap (renderList' . map (align . renderDoc doesntNeedParens)) err
-- "simple" here means that at least one is true:
--
-- * one of the lists is empty
-- * rendered representation for both expected and actual types are single-line
simple = any null err || all (null . drop 1 . LT.lines . printDoc False) errText
renderList' ds
= case ds of
[] -> "[]"
[d] -> align $ "[ " <> d <> " ]"
_ -> align $ vcat (zipWith (<>) ("[ " : repeat ", ") ds) <$> "]"
diff :: MismatchError Doc -> Doc
diff (fmap $ LT.lines . printDoc False -> MkMismatchError{..}) = vcat $
showLine `fmap` getDiffBy ((==) `on` LT.strip) meExpected meActual
where
showLine = \case
(Both _ b) -> indent 2 $ text b
(First x) -> "-" <+> text x
(Second x) -> "+" <+> text x