morley-1.19.2: 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(..)
, buildDocDiff
, buildDocDiffList
) where
import Prelude hiding (First, (<$>))
import Data.Algorithm.Diff (PolyDiff(..), getDiffBy)
import Data.Text.Lazy as LT (lines, strip)
import Fmt (Buildable(..), Doc, unlessF, (<+>))
import Prettyprinter (align, fill, hardline, indent, line, 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 = buildStandard
buildStandard :: Buildable a => MismatchError a -> Doc
buildStandard = buildDoc . fmap build
buildStandardList :: Buildable a => MismatchError [a] -> Doc
buildStandardList = buildDoc . fmap buildList
buildDoc :: MismatchError Doc -> Doc
buildDoc MkMismatchError{..}
= fill 9 "Expected:" <+> align meExpected <> hardline
<> fill 9 "Actual:" <+> align meActual
renderMismatch :: MismatchError Doc -> Doc
renderMismatch errText
= hardline <> "Mismatch:" <> hardline
<> indent 2 "--- expected +++ actual" <> hardline
<> align (diff errText)
-- | Render a mismatch error with a diff.
--
-- This is intended to be used with types for which 'RenderDoc' outputs
-- multiline 'Doc'.
buildDocDiff :: Buildable a => MismatchError a -> Doc
buildDocDiff err = buildStandard err
<> unlessF expectedAndActualBothSingleLine (renderMismatch errText)
where
errText = fmap build err
expectedAndActualBothSingleLine = all (null . drop 1 . LT.lines . printRenderDoc 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'.
buildDocDiffList :: Buildable a => MismatchError [a] -> Doc
buildDocDiffList err = buildStandardList err
<> unlessF simple (renderMismatch errText)
where
errText = fmap (renderList' . map (align . build)) 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 . printRenderDoc False) errText
renderList' ds
= case ds of
[] -> "[]"
[d] -> align $ "[ " <> d <> " ]"
_ -> align $ vcat (zipWith (<>) ("[ " : repeat ", ") ds) <> line <> "]"
diff :: MismatchError Doc -> Doc
diff (fmap $ LT.lines . printRenderDoc False -> MkMismatchError{..}) = vcat $
showLine `fmap` getDiffBy ((==) `on` LT.strip) meExpected meActual
where
showLine = \case
(Both _ b) -> indent 2 $ build b
(First x) -> "-" <+> build x
(Second x) -> "+" <+> build x