packages feed

lorentz-0.2.0: src/Lorentz/Errors/Numeric/Doc.hs

-- | Autodoc for numeric errors.
module Lorentz.Errors.Numeric.Doc
  ( DDescribeErrorTagMap (..)
  , applyErrorTagToErrorsDoc
  , applyErrorTagToErrorsDocWith
  , NumericErrorDocHandler
  , NumericErrorDocHandlerError
  , customErrorDocHandler
  , voidResultDocHandler
  , baseErrorDocHandlers

    -- * Internals
  , NumericErrorWrapper
  ) where

import Control.Monad.Cont (callCC, runCont)
import qualified Data.Bimap as Bimap
import Data.Constraint (Dict(..))
import qualified Data.Kind as Kind
import Data.Typeable ((:~:)(..), eqT)
import Data.Typeable (typeRep)
import Fmt (build, pretty)
import GHC.TypeNats (KnownNat, Nat, natVal, someNatVal)

import Lorentz.Base
import Lorentz.Doc
import Lorentz.Errors
import Lorentz.Errors.Numeric.Contract
import Lorentz.Macro
import Michelson.Text (MText)
import Michelson.Typed
import Util.Markdown
import Util.Typeable

-- | Adds a section which explains error tag mapping.
data DDescribeErrorTagMap = DDescribeErrorTagMap
  { detmSrcLoc :: Text
    -- ^ Describes where the error tag map is defined in Haskell code.
  } deriving (Eq, Ord)

instance DocItem DDescribeErrorTagMap where
  type DocItemPosition DDescribeErrorTagMap = 4090
  type DocItemPlacement DDescribeErrorTagMap = 'DocItemInDefinitions
  docItemSectionName = Just "About error tags mapping"
  docItemRef DDescribeErrorTagMap{..} = DocItemRef $
    DocItemId $ "error-mapping-" <> detmSrcLoc
  docItemToMarkdown _ DDescribeErrorTagMap{..} = [md|
    This contract uses numeric representation of error tags.
    Nevertheless, the original Lorentz code operates with string tags which are
    later mapped to naturals.

    If you need to handle errors produced by this contract, we recommend
    converting numeric tags back to strings first, preserving textual tags which
    are already present, and then pattern-match on textual tags.
    This conversion can be performed with the help of the error tag map defined
    at `#{detmSrcLoc}`.

    Note that some errors can still use a textual tag, for instance to
    satisfy rules of an interface.

    In [TM-376](https://issues.serokell.io/issue/TM-376) we are going to provide
    more type-safe and convenient mechanisms for errors handling.
    |]
    -- TODO [TM-376]: update the comment above on how to work with our errors
    -- from Haskell

-- | Anchor which refers to the section describing error tag mapping.
dDescribeErrorTagMapAnchor :: Anchor
dDescribeErrorTagMapAnchor = Anchor "about-error-tags-mapping"

-- | Errors for 'NumericErrorDocHandler'
data NumericErrorDocHandlerError
  = EheNotApplicable
    -- ^ Given handler is not suitable, probably another one will fit.
  | EheConversionUnnecessary
    -- ^ Given handler suits and tells that given error should remain unchanged.

data SomeErrorWithDoc = forall err. ErrorHasDoc err => SomeErrorWithDoc (Proxy err)

-- | Handler which changes documentation for one particular error type.
newtype NumericErrorDocHandler =
  NumericErrorDocHandler
  { _unNumericErrorDocHandler
      :: forall givenErr.
         (ErrorHasDoc givenErr)
      => ErrorTagMap
      -> Proxy givenErr
      -> Either NumericErrorDocHandlerError SomeErrorWithDoc
  }

-- | Modify documentation generated for given code so that all 'CustomError'
-- mention not their textual error tag rather respective numeric one from the
-- given map.
--
-- If some documented error is not present in the map, it remains unmodified.
-- This function may fail with 'error' if contract uses some uncommon errors,
-- see 'applyErrorTagToErrorsDocWith' for details.
applyErrorTagToErrorsDoc
  :: HasCallStack
  => ErrorTagMap -> inp :-> out -> inp :-> out
applyErrorTagToErrorsDoc = applyErrorTagToErrorsDocWith baseErrorDocHandlers

-- | Extended version of 'applyErrorTagToErrorsDoc' which accepts error
-- handlers.
--
-- In most cases that function should be enough for your purposes, but it uses
-- a fixed set of base handlers which may be not enough in case when you define
-- your own errors. In this case define and pass all the necessary handlers to
-- this function.
--
-- It fails with 'error' if some of the errors used in the contract cannot be
-- handled with given handlers.
applyErrorTagToErrorsDocWith
  :: HasCallStack
  => [NumericErrorDocHandler]
  -> ErrorTagMap
  -> inp :-> out
  -> inp :-> out
applyErrorTagToErrorsDocWith handlers errorTagMap =
  iMapAnyCode $ modifyInstrDoc @_ @DThrows $
  \(DThrows ep) ->
    flip runCont id $
    callCC $ \quitWith -> do
      forM_ handlers $ \(NumericErrorDocHandler handler) ->
        case handler errorTagMap ep of
          Left EheNotApplicable -> pass
          Left EheConversionUnnecessary -> quitWith Nothing
          Right (SomeErrorWithDoc nep) -> quitWith $ Just (DThrows nep)
      error $ "No handler found for error " <> show (typeRep ep)

mkGeneralNumericWrapper
  :: forall err.
     (ErrorHasDoc err, ErrorHasNumericDoc err)
  => ErrorTagMap
  -> MText
  -> Either NumericErrorDocHandlerError SomeErrorWithDoc
mkGeneralNumericWrapper errorTagMap strTag = do
  numErrTag <- Bimap.lookupR strTag errorTagMap
              & maybeToRight EheConversionUnnecessary
  SomeNat (_ :: Proxy numTag) <- pure $ someNatVal numErrTag
  return $ SomeErrorWithDoc $ Proxy @(NumericErrorWrapper numTag err)

-- | Handler for all 'CustomError's.
customErrorDocHandler :: NumericErrorDocHandler
customErrorDocHandler = NumericErrorDocHandler $
  \errorTagMap (_ :: Proxy givenErr) ->
    join . maybeToRight EheNotApplicable $
    eqTypeIgnoringPhantom @CustomError @givenErr $ \Refl (_ :: Proxy strTag) ->
      case errorDocRequirements @(CustomError strTag) of
        Dict -> do
          let strTag = errorTagToMText (fromLabel @strTag)
          mkGeneralNumericWrapper @givenErr errorTagMap strTag

-- | Handler for 'VoidResult'.
voidResultDocHandler :: NumericErrorDocHandler
voidResultDocHandler = NumericErrorDocHandler $
  \errorTagMap (_ :: Proxy givenErr) ->
    join . maybeToRight EheNotApplicable $
    eqTypeIgnoringPhantom @VoidResult @givenErr $ \Refl (_ :: Proxy res) -> do
      mkGeneralNumericWrapper @givenErr errorTagMap voidResultTag

-- | Handler for textual error messages.
textErrorDocHandler :: NumericErrorDocHandler
textErrorDocHandler = NumericErrorDocHandler $
  \_errorTagMap (_ :: Proxy givenErr) ->
    case eqT @givenErr @MText of
      Nothing -> Left EheNotApplicable
      Just Refl -> pure $ SomeErrorWithDoc $ Proxy @NumericTextError

-- | Handlers for most common errors defined in Lorentz.
baseErrorDocHandlers :: [NumericErrorDocHandler]
baseErrorDocHandlers =
  [ customErrorDocHandler
  , voidResultDocHandler
  , textErrorDocHandler
  ]

-- | Pseudo error which stands for textual errors converted to numeric codes.
data NumericTextError

instance ErrorHasDoc NumericTextError where
  errorDocName = errorDocName @MText
  errorDocMdCause = errorDocMdCause @MText
  errorDocMdCauseInEntrypoint = errorDocMdCauseInEntrypoint @MText
  errorDocClass = errorDocClass @MText
  errorDocDependencies = [SomeDocDefinitionItem (DType $ Proxy @Natural)]
  errorDocHaskellRep =
    "Numeric code for an error message, see also " <>
    mdLocalRef "error tags mapping" dDescribeErrorTagMapAnchor <> "."

-- | Some error with a numeric tag attached.
data NumericErrorWrapper (numTag :: Nat) (err :: Kind.Type)

instance ( ErrorHasDoc err
         , KnownNat numTag, ErrorHasNumericDoc err
         ) =>
         ErrorHasDoc (NumericErrorWrapper numTag err) where
  errorDocName = errorDocName @err
  errorDocMdCause = errorDocMdCause @err
  errorDocMdCauseInEntrypoint = errorDocMdCauseInEntrypoint @err
  errorDocClass = errorDocClass @err
  errorDocDependencies = errorDocDependencies @err
  errorDocHaskellRep =
    case errorDocRequirements @err of
      Dict -> mconcat
        [ let numTag = pretty (natVal $ Proxy @numTag) <> " :: nat"
          in numericErrorDocHaskellRep @err numTag
        , "\n\n"
        , mdSubsection "Respective textual tag" $
            mdTicked (build $ numericErrorDocTextualTag @err)
        ]

-- | Helper typeclass which overloads representation for errors.
class ErrorHasNumericDoc err where
  -- | Error representation with respect to tags being changed to numeric ones.
  numericErrorDocHaskellRep :: ErrorRequirements err => Text -> Markdown
  numericErrorDocTextualTag :: ErrorRequirements err => Text

instance ErrorHasNumericDoc (CustomError tag) where
  numericErrorDocHaskellRep numTag =
    customErrorDocHaskellRepGeneral numTag (Proxy @tag)
  numericErrorDocTextualTag = errorTagToText @tag

instance ErrorHasDoc (VoidResult res) =>
         ErrorHasNumericDoc (VoidResult res) where
  numericErrorDocHaskellRep numTag =
    case errorDocRequirements @(VoidResult res) of
      Dict -> mdTicked $ "(" <> build numTag <> ", " <> "<return value>" <> ")"
  numericErrorDocTextualTag = toText voidResultTag