packages feed

simple-parser-0.12.0: src/SimpleParser/Explain.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}

module SimpleParser.Explain
  ( TextBuildable (..)
  , ShowTextBuildable (..)
  , ExplainLabel (..)
  , ErrorExplanation (..)
  , ExplainError (..)
  , Explainable
  , ParseErrorExplanation (..)
  , explainParseError
  , buildParseErrorExplanation
  , buildAllParseErrorExplanations
  ) where

import Control.Monad (join)
import Data.Foldable (toList)
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.Text (Text)
import Data.Void (Void, absurd)
import SimpleParser.Common (CompoundTextLabel (..), TextLabel (..))
import SimpleParser.Result (CompoundError (..), ParseError (..), RawError (..), StreamError (..),
                            parseErrorEnclosingLabels, parseErrorNarrowestSpan)
import SimpleParser.Stream (HasLinePos (..), PosStream (..), Span (..), Stream (..))
import Text.Builder (Builder)
import qualified Text.Builder as TB

-- | Types that can be rendered into a textual error message
-- (Effectively a fancy Show)
class TextBuildable a where
  buildText :: a -> Builder

instance TextBuildable Char where
  buildText = TB.char

instance TextBuildable String where
  buildText = TB.string

instance TextBuildable Text where
  buildText = TB.text

instance TextBuildable Builder where
  buildText = id

buildTextFromList :: TextBuildable a => [a] -> Builder
buildTextFromList ss = "[" <> TB.intercalate ", " (fmap buildText ss) <> "]"

instance TextBuildable a => TextBuildable [a] where
  buildText = buildTextFromList

instance TextBuildable a => TextBuildable (Seq a) where
  buildText = buildTextFromList . toList

-- | Deriving-Via wrapper for 'TextBuildable' for types with 'Show'
newtype ShowTextBuildable a = ShowTextBuildable { unShowTextBuildable :: a }

instance Show a => TextBuildable (ShowTextBuildable a) where
  buildText = TB.string . show . unShowTextBuildable

class ExplainLabel l where
  explainLabel :: l -> Builder

  explainLabelText :: l -> Text
  explainLabelText = TB.run . explainLabel

instance ExplainLabel Void where
  explainLabel = absurd

instance ExplainLabel TextLabel where
  explainLabel l =
    case l of
      TextLabelSpace -> "space"
      TextLabelHSpace -> "non-line-breaking space"
      TextLabelDigit -> "digit"

instance ExplainLabel l => ExplainLabel (CompoundTextLabel l) where
  explainLabel c =
    case c of
      CompoundTextLabelText l -> explainLabel l
      CompoundTextLabelCustom l -> explainLabel l

data ErrorExplanation  = ErrorExplanation
  { eeReason :: !Text
  , eeExpected :: !(Maybe Text)
  , eeActual :: !(Maybe Text)
  } deriving (Eq, Show)

class ExplainError e where
  explainError :: e -> ErrorExplanation

instance ExplainError Void where
  explainError = absurd

endMsg :: Text
endMsg = "end of stream"

tokB :: TextBuildable a => a -> Builder
tokB t = "token '" <> buildText t <> "'"

tokT :: TextBuildable a => a -> Text
tokT = TB.run . tokB

mayTokT :: TextBuildable a => Maybe a -> Text
mayTokT = maybe endMsg tokT

chunkB :: TextBuildable a => a -> Builder
chunkB k = "chunk \"" <> buildText k <> "\""

chunkT :: TextBuildable a => a -> Text
chunkT = TB.run . chunkB

mayChunkT :: TextBuildable a => Maybe a -> Text
mayChunkT = maybe endMsg chunkT

instance (TextBuildable (Token s), TextBuildable (Chunk s)) => ExplainError (StreamError s) where
  explainError (StreamError re) =
    case re of
      RawErrorMatchEnd actTok ->
        ErrorExplanation "failed to match end of stream" (Just endMsg) (Just (tokT actTok))
      RawErrorAnyToken ->
        ErrorExplanation "failed to match any token" (Just "any token") (Just endMsg)
      RawErrorAnyChunk ->
        ErrorExplanation "failed to match any chunk" (Just "any chunk") (Just endMsg)
      RawErrorSatisfyToken mayActTok ->
        ErrorExplanation "failed to satisfy token predicate" Nothing (Just (mayTokT mayActTok))
      RawErrorMatchToken expTok mayActTok ->
        ErrorExplanation "failed to match token" (Just (tokT expTok)) (Just (mayTokT mayActTok))
      RawErrorMatchChunk expChunk mayActChunk ->
        ErrorExplanation "failed to match chunk" (Just (chunkT expChunk)) (Just (mayChunkT mayActChunk))
      RawErrorTakeTokensWhile1 mayActTok ->
        ErrorExplanation "failed to take 1 or more tokens" Nothing (Just (mayTokT mayActTok))
      RawErrorDropTokensWhile1 mayActTok ->
        ErrorExplanation "failed to drop 1 or more tokens" Nothing (Just (mayTokT mayActTok))

instance (TextBuildable (Token s), TextBuildable (Chunk s), ExplainError e) => ExplainError (CompoundError s e) where
  explainError ce =
    case ce of
      CompoundErrorStream se -> explainError se
      CompoundErrorFail msg -> ErrorExplanation msg Nothing Nothing
      CompoundErrorCustom e -> explainError e

type Explainable l s e = (PosStream s, ExplainLabel l, ExplainError e)

data ParseErrorExplanation p = ParseErrorExplanation
  { peeSpan :: !(Span p)
  , peeContext :: !(Seq Text)
  , peeDetails :: !(Maybe Text)
  , peeErrExp :: !ErrorExplanation
  } deriving (Eq, Show)

explainParseError :: (TextBuildable (Token s), TextBuildable (Chunk s), Explainable l s e) => ParseError l s e -> ParseErrorExplanation (Pos s)
explainParseError pe =
  let (mayLab, sp) = parseErrorNarrowestSpan pe
      context = fmap explainLabelText (parseErrorEnclosingLabels pe)
      mayDetails = fmap explainLabelText mayLab
      errExp = explainError (peError pe)
  in ParseErrorExplanation sp context mayDetails errExp

buildSpan :: HasLinePos p => Span p -> Builder
buildSpan (Span p1 p2) =
  let l1 = viewLine p1
      c1 = viewCol p1
      l2 = viewLine p2
      c2 = viewCol p2
      r1 = TB.decimal (succ l1) <> ":" <> TB.decimal (succ c1)
      r2 = TB.decimal (succ l2) <> ":" <> TB.decimal (succ c2)
  in if l1 == l2 && c1 == c2
    then r1
    else r1 <> "-" <> r2

buildErrorExplanation :: Maybe Builder -> ErrorExplanation -> [Builder]
buildErrorExplanation mayDetails (ErrorExplanation reason mayExpected mayActual) = join
  [ ["[Reason  ] " <> TB.text reason]
  , maybe [] (\de -> ["[Details ] " <> de]) mayDetails
  , maybe [] (\ex -> ["[Expected] " <> TB.text ex]) mayExpected
  , maybe [] (\ac -> ["[Actual  ] " <> TB.text ac]) mayActual
  ]

buildParseErrorExplanation :: HasLinePos p => ParseErrorExplanation p -> Builder
buildParseErrorExplanation (ParseErrorExplanation sp context mayDetails errExp) =
  let hd = join
        [ ["[Pos     ] " <> buildSpan sp]
        , ["[Context ] " <> TB.intercalate " > " (fmap TB.text (toList context)) | not (Seq.null context)]
        ]
      tl = buildErrorExplanation (fmap TB.text mayDetails) errExp
  in TB.intercalate "\n" (hd ++ tl)

buildAllParseErrorExplanations :: (HasLinePos p, Foldable f) => f (ParseErrorExplanation p) -> Builder
buildAllParseErrorExplanations = TB.intercalate "\n\n" . fmap buildParseErrorExplanation . toList