packages feed

lorentz-0.13.3: src/Lorentz/ViewBase.hs

-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

module Lorentz.ViewBase
  ( ViewTyInfo(..)
  , type (?::)
  , type (>->)
  , ViewsList
  , RevealViews
  , LookupView
  , LookupRevealView
  , HasView
  , ViewsNames

  , ViewName (..)
  , ViewInterface (..)
  , demoteViewName
  , demoteViewTyInfos
  , DemoteViewsDescriptor
  , demoteViewsDescriptor
  , ViewInterfaceMatchError (..)
  , checkViewsCoverInterface
  ) where

import Control.Monad.Except (throwError)
import Data.Map qualified as Map
import Data.Singletons (demote)
import Fmt (Buildable(..), pretty, (+|), (|+))

import Morley.Michelson.Typed
import Morley.Util.MismatchError
import Morley.Util.TypeLits

-- | Type-level information about a view.
data ViewTyInfo = ViewTyInfo Symbol Type Type

-- | Neat constructor for 'ViewTyInfo'.
--
-- type View = "view" ?:: Integer >-> Natural
type family (?::) (name :: Symbol) (tys :: (Type, Type)) where
  name ?:: '(arg, ret) = 'ViewTyInfo name arg ret
infix 3 ?::

type arg >-> ret = '(arg, ret)
infix 5 >->

-- | Get a list of views by a descriptor object.
--
-- The problem this type family solves:
-- it is unpleasant to carry around a list of views because it may be large,
-- and if we merely hide this list under a type alias, error messages will still
-- mention the type alias expanded.
-- We want e.g. @Contract Parameter Storage Views@ to be carried as-is.
-- @Parameter@ and @Storage@ are usually datatypes and they are fine, while
-- for @Views@ to be not automatically expanded we have to take special care.
--
-- You can still provide the list of 'ViewTyInfo's to this type family using
-- t'ViewsList', but generally prefer creating a dedicated datatype that would
-- expand to a views list.
type family RevealViews (desc :: Type) :: [ViewTyInfo]

-- | A views descriptor that directly carries the full list of views.
data ViewsList (vl :: [ViewTyInfo])

type instance RevealViews () = '[]
type instance RevealViews (ViewsList info) = info

-- | Find a view in a contract by name.
type family LookupView (name :: Symbol) (views :: [ViewTyInfo]) :: (Type, Type) where
  LookupView name '[] =
    TypeError ('Text "View " ':<>: 'ShowType name ':<>: 'Text " is not found")
  LookupView name ('ViewTyInfo name arg ret ': _) = '(arg, ret)
  LookupView name ('ViewTyInfo _ _ _ ': vs) = LookupView name vs

-- | Reveal views and find a view there.
type LookupRevealView name viewRef = LookupView name (RevealViews viewRef)

-- | Constraint indicating that presence of the view with the specified
-- parameters is implied by the views descriptor.
type HasView vd name arg ret = (LookupRevealView name vd ~ '(arg, ret))

-- | Map views to get their names.
type family ViewsNames (vs :: [ViewTyInfo]) :: [Symbol] where
  ViewsNames '[] = '[]
  ViewsNames ('ViewTyInfo name _ _ ': vs) = name ': ViewsNames vs

-- | Demote view name from type level to term level.
demoteViewName
  :: forall name.
     (KnownSymbol name, HasCallStack)
  => ViewName
demoteViewName = unsafe . mkViewName $ symbolValT' @name

-- | Interface of a single view at term-level.
data ViewInterface = ViewInterface
  { viName :: ViewName
  , viArg :: T
  , viRet :: T
  }

-- | Helper typeclass to demote 'ViewTyInfo's to 'ViewInterface's.
class DemoteViewTyInfo (vs :: [ViewTyInfo]) where
  demoteViewTyInfos' :: Proxy vs -> [ViewInterface]

instance DemoteViewTyInfo '[] where
  demoteViewTyInfos' _ = []
instance (KnownSymbol name, IsoValue arg, IsoValue ret, DemoteViewTyInfo vs) =>
         DemoteViewTyInfo ('ViewTyInfo name arg ret ': vs) where
  demoteViewTyInfos' _ =
    ViewInterface
    { viName = demoteViewName @name
    , viArg = demote @(ToT arg)
    , viRet = demote @(ToT ret)
    } : demoteViewTyInfos' (Proxy @vs)

-- | Demote 'ViewTyInfo's to 'ViewInterface's.
demoteViewTyInfos
  :: forall (vs :: [ViewTyInfo]). DemoteViewTyInfo vs
  => [ViewInterface]
demoteViewTyInfos = demoteViewTyInfos' (Proxy @vs)

type DemoteViewsDescriptor vd = DemoteViewTyInfo (RevealViews vd)

-- | Demote views descriptor to 'ViewInterface's.
demoteViewsDescriptor
  :: forall (vd :: Type). DemoteViewTyInfo (RevealViews vd)
  => [ViewInterface]
demoteViewsDescriptor = demoteViewTyInfos @(RevealViews vd)

data ViewInterfaceMatchError
  = VIMViewNotFound ViewName
  | VIMViewArgMismatch (MismatchError T)
  | VIMViewRetMismatch (MismatchError T)
  deriving stock (Show, Eq)

instance Buildable ViewInterfaceMatchError where
  build = \case
    VIMViewNotFound name ->
      "View not found in the contract " +| name |+ ""
    VIMViewArgMismatch merr ->
      "View argument type mismatch.\n" +| merr |+ ""
    VIMViewRetMismatch merr ->
      "View return type mismatch.\n" +| merr |+ ""

instance Exception ViewInterfaceMatchError where
  displayException = pretty

-- | Check that the given set of views covers the given view interfaces.
-- Extra views in the set, that do not appear in the interface, are fine.
checkViewsCoverInterface
  :: forall st. [ViewInterface] -> ViewsSet st -> Either ViewInterfaceMatchError ()
checkViewsCoverInterface interfaces (ViewsList views) =
  forM_ interfaces $ \ViewInterface{..} -> do
    () <- case Map.lookup viName viewsMap of
      Nothing -> throwError (VIMViewNotFound viName)
      Just (SomeView View{..}) -> do
        unless (notesT vArgument == viArg) $
          throwError $ VIMViewArgMismatch MkMismatchError
            { meExpected = viArg, meActual = notesT vArgument }
        unless (notesT vReturn == viRet) $
          throwError $ VIMViewRetMismatch MkMismatchError
            { meExpected = viRet, meActual = notesT vReturn }
    pass
  where
    viewsMap :: Map.Map ViewName (SomeView st)
    viewsMap = Map.fromList [ (vName, v) | v@(SomeView View{..}) <- views ]