packages feed

prosidyc-0.1.0.0: src/Prosidy/Compile/Error.hs

{-|
Module      : Prosidy.Compile.Error
Description : Error definitions and utility functions.
Copyright   : ©2020 James Alexander Feldman-Crough
License     : MPL-2.0
Maintainer  : alex@fldcr.com
-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
module Prosidy.Compile.Error
    ( Error(..)
    , TagKind(..)
    , ErrorSet
    , Error'
    , ErrorSet'
    , IsError
    , MonadErrors
    , singleError
    , customError
    , throwError1
    , allErrors
    , attachLocation
    , MonadError(..)
    )
where

import           Lens.Micro

import           Control.Exception              ( Exception(..) )
import           Prosidy.Types.Key              ( Key )
import           Prosidy.Source                 ( Line(..)
                                                , Column(..)
                                                , Location
                                                )
import           Prosidy.Optics.Source          ( HasLocation(..)
                                                , line
                                                , column
                                                )
import           Data.HashSet                   ( HashSet
                                                , singleton
                                                )
import           Control.Monad.Except           ( MonadError(..)
                                                , throwError
                                                )
import           GHC.Generics                   ( Generic )
import           Data.Hashable                  ( Hashable )
import           Data.Typeable                  ( Typeable )
import           Data.Void                      ( Void )
import           Data.Foldable                  ( toList )
import           Data.List.NonEmpty             ( NonEmpty(..)
                                                , nonEmpty
                                                )

-- | A contraint alias for types returning at least one error.
type MonadErrors e = MonadError (ErrorSet e)

-- | A constraint alias for errors throwable in a context admitting a 
-- 'MonadErrors' instance.
type IsError e = (Exception e, Hashable e, Eq e)

-- | A non-empty set of errors. 
newtype ErrorSet e =
    ErrorSet (HashSet (Error e))
  deriving stock (Show, Generic, Eq)
  deriving anyclass (Hashable)

instance IsError e => Semigroup (ErrorSet e) where
    ErrorSet lhs <> ErrorSet rhs = ErrorSet $! lhs <> rhs

-- | A type alias for 'ErrorSet's which never contain empty errors.
type ErrorSet' = ErrorSet Void

-- | Enumerates the errors thrown when 
data Error a =
    Custom a
    -- ^ A custom error, allowing extensibility.
  | ParseError Key String
    -- ^ Thrown when parsing a setting fails.
  | Required Key
    -- ^ Thrown when a setting was required to be set, but wasn't provided.
  | ExpectedTag TagKind Key
    -- ^ Thrown when matching against a 'Prosidy.Tag', and another node was 
    -- found, or the input tag's 'Key' didn't match the specified key.
  | ExpectedParagraph
    -- ^ Thrown when matching against paragraph and an unexpected node was
    -- encountered.
  | ExpectedText
    -- ^ Thrown when matching against text and an unexpected node was
    -- encountered.
  | ExpectedBreak
    -- ^ Thrown when matching against an explicit break and an unexpected node
    -- was encountered.
  | EmptyMatch
    -- ^ Thrown when a match has no cases to check against.
  | Group (Maybe Location) (ErrorSet a)
    -- ^ Used to group a set of errors thrown at the same point in a tree.
    -- If a location is available, we attach it for debugging.
  deriving (Eq, Show, Generic, Hashable)

instance (Typeable a, Exception a) => Exception (Error a) where
    displayException (Custom a        ) = displayException a

    displayException (ParseError k msg) = mconcat
        [ showString "failed to parse the setting "
        , shows k
        , showString ": "
        , showString msg
        ]
        ""

    displayException EmptyMatch   = "Match provided with no possible cases."

    displayException (Required k) = "missing required setting " <> show k

    displayException (ExpectedTag kind k) =
        "expected a " <> show kind <> " tag with key " <> show k

    displayException ExpectedParagraph    = "expected a paragrapgh"

    displayException ExpectedText         = "expected plain text"

    displayException ExpectedBreak        = "expected a break"

    displayException (Group (Just loc) x) = mconcat
        [ showString "error(s) encountered at line "
        , shows (loc ^?! line . to (\(Line n) -> succ n))
        , showString " column "
        , shows (loc ^?! column . to (\(Column n) -> succ n))
        , showString ":\n"
        , foldMap
            (\exn -> showString (displayException exn) <> showChar '\n')
            (allErrors x)
        ]
        ""

    displayException (Group Nothing x) = foldMap
        (\exn -> showString (displayException exn) <> showChar '\n')
        (allErrors x)
        ""

-- | A type alias for 'Error's that never throw a custom error.
type Error' = Error Void

-- | A marker class for marking which type of tag 'ExpectedTag' was expecting.
data TagKind = BlockKind | InlineKind | LiteralKind
  deriving (Show, Eq, Generic, Hashable)

-- | Group errors together, attaching a location if one is available.
attachLocation :: (IsError e, MonadErrors e m, HasLocation l) => l -> m a -> m a
attachLocation item = flip catchError $ throwError1 . Group (item ^? location)

-- | Lift a single 'Error' into an 'ErrorSet'.
singleError :: Hashable e => Error e -> ErrorSet e
singleError = ErrorSet . singleton
{-# INLINE singleError #-}

-- | Lift a custom error into an 'ErrorSet'.
customError :: Hashable e => e -> ErrorSet e
customError = singleError . Custom
{-# INLINE customError #-}

-- | Throw a single error.
throwError1 :: Hashable e => MonadErrors e m => Error e -> m a
throwError1 = throwError . singleError
{-# INLINE throwError1 #-}

-- | Return the set of errors in an 'ErrorSet' as a non-empty list.
allErrors :: ErrorSet e -> NonEmpty (Error e)
allErrors (ErrorSet hs) =
    maybe (error "unexpected empty ErrorSet") id . nonEmpty $ toList hs