packages feed

morley-1.2.0: src/Michelson/Untyped/Annotation.hs

-- | Michelson annotations in untyped model.

module Michelson.Untyped.Annotation
  ( Annotation (..)
  , pattern Annotation
  , pattern WithAnn

  -- * Annotation Set
  , AnnotationSet
  , emptyAnnSet
  , fullAnnSet
  , isNoAnnSet
  , minAnnSetSize
  , singleAnnSet
  , singleGroupAnnSet

  -- * Rendering
  , KnownAnnTag(..)
  , TypeAnn
  , FieldAnn
  , VarAnn
  , SomeAnn
  , RootAnn

  -- * Creation and conversions
  , noAnn
  , ann
  , mkAnnotation
  , specialVarAnns
  , specialFieldAnn
  , isValidAnnStart
  , isValidAnnBodyChar
  , unifyAnn
  , ifAnnUnified
  , disjoinVn
  , convAnn
  ) where

import Data.Aeson.TH (deriveJSON)
import Data.Char (isAlpha, isAscii, isNumber)
import Data.Data (Data(..))
import Data.Default (Default(..))
import qualified Data.Kind as Kind
import qualified Data.Text as T
import Data.Typeable ((:~:)(..), eqT)
import Fmt (Buildable(build))
import Instances.TH.Lift ()
import Language.Haskell.TH.Lift (deriveLift)
import Text.PrettyPrint.Leijen.Text (Doc, hsep, textStrict, (<+>))
import qualified Text.Show

import Michelson.Printer.Util (RenderDoc(..), buildRenderDoc, doesntNeedParens, printDocS)
import Util.Aeson

-- | Generic Type/Field/Variable Annotation
--
-- As per Michelson documentation, this type has an invariant:
-- (except for the first character, here parametrized in the type `tag`) the
-- allowed character set is the one matching the following regexp:
-- @%|@%%|%@|[@:%][_a-zA-Z][_0-9a-zA-Z\.%@]*
newtype Annotation tag = AnnotationUnsafe { unAnnotation :: Text }
  deriving stock (Eq, Data, Functor, Generic)
  deriving newtype (IsString)

instance NFData (Annotation tag)

pattern Annotation :: Text -> Annotation tag
pattern Annotation ann <- AnnotationUnsafe ann

{-# COMPLETE Annotation :: Annotation #-}

instance Default (Annotation tag) where
  def = noAnn

--------------------------------------------------------------------------------
-- Annotation Set
--------------------------------------------------------------------------------

-- | An 'AnnotationSet' contains all the type/field/variable 'Annotation's
-- , with each group in order, associated with an entity.
-- Note that in its rendering/show instances the unnecessary annotations will be
-- omitted, as well as in some of the functions operating with it.
-- Necessary 'Annotation's are the ones strictly required for a consistent
-- representation.
-- In particular, for each group (t/f/v):
--   - if all annotations are 'noAnn' they are all omitted
--   - if one or more 'noAnn' follow a non-empty 'ann', they are omitted
--   - if one or more 'noAnn' precede a non-empty 'ann', they are kept
--   - every non-empty 'ann' is obviously kept
-- This is why order for each group is important as well as separation of
-- different groups of 'Annotation's.
data AnnotationSet = AnnotationSet
  { asTypes  :: [TypeAnn]
  , asFields :: [FieldAnn]
  , asVars   :: [VarAnn]
  } deriving stock Eq

instance Semigroup AnnotationSet where
  (AnnotationSet ts1 fs1 vs1) <> (AnnotationSet ts2 fs2 vs2) = AnnotationSet {..}
    where
      asTypes  = ts1 <> ts2
      asFields = fs1 <> fs2
      asVars   = vs1 <> vs2

instance Monoid AnnotationSet where
  mempty = emptyAnnSet

-- | An 'AnnotationSet' without any 'Annotation'.
emptyAnnSet :: AnnotationSet
emptyAnnSet = AnnotationSet [] [] []

-- | An 'AnnotationSet' with only a single 'Annotation' (of any kind).
singleAnnSet :: forall tag. KnownAnnTag tag => Annotation tag -> AnnotationSet
singleAnnSet an = singleGroupAnnSet [an]

-- | An 'AnnotationSet' with several 'Annotation's of the same kind.
singleGroupAnnSet :: forall tag. KnownAnnTag tag => [Annotation tag] -> AnnotationSet
singleGroupAnnSet ans = AnnotationSet {..}
  where
    asTypes = case eqT @tag @TypeTag of Just Refl -> ans; Nothing -> []
    asFields = case eqT @tag @FieldTag of Just Refl -> ans; Nothing -> []
    asVars = case eqT @tag @VarTag of Just Refl -> ans; Nothing -> []

-- | An 'AnnotationSet' built from all 3 kinds of 'Annotation'.
fullAnnSet :: [TypeAnn] -> [FieldAnn] -> [VarAnn] -> AnnotationSet
fullAnnSet asTypes asFields asVars = AnnotationSet {..}

-- | Returns 'True' if all 'Annotation's in the Set are unnecessary/empty/'noAnn'.
-- False otherwise.
isNoAnnSet :: AnnotationSet -> Bool
isNoAnnSet annSet = null asTypes && null asFields && null asVars
  where AnnotationSet {..} = minimizeAnnSet annSet

-- | Returns the amount of 'Annotation's that are necessary for a consistent
-- representation. See 'AnnotationSet'.
minAnnSetSize :: AnnotationSet -> Int
minAnnSetSize annSet = length asTypes + length asFields + length asVars
  where AnnotationSet {..} = minimizeAnnSet annSet

-- | Removes all unnecessary 'Annotation's. See 'AnnotationSet'.
minimizeAnnSet :: AnnotationSet -> AnnotationSet
minimizeAnnSet (AnnotationSet ts fs vs) = AnnotationSet {..}
  where
    asTypes = trimEndNoAnn ts
    asFields = trimEndNoAnn fs
    asVars = trimEndNoAnn vs

-- | Removes all unnecessary 'Annotation's from a list of the same type
trimEndNoAnn :: [Annotation tag] -> [Annotation tag]
trimEndNoAnn = foldr (\a lst -> if null lst && a == noAnn then [] else a : lst) []

--------------------------------------------------------------------------------
-- Rendering
--------------------------------------------------------------------------------

class Typeable (tag :: Kind.Type) => KnownAnnTag tag where
  annPrefix :: Text

instance KnownAnnTag tag => Show (Annotation tag) where
  show = printDocS True . renderDoc doesntNeedParens

data TypeTag
data FieldTag
data VarTag
data SomeTag

data RootTag

type TypeAnn = Annotation TypeTag
type FieldAnn = Annotation FieldTag
type VarAnn = Annotation VarTag
type SomeAnn = Annotation SomeTag

-- | Root annotation was added in the Babylon, it looks the same as
-- field annotation, but has slightly different semantic and can be used
-- only in parameter 'ParameterType'.
type RootAnn = Annotation RootTag

instance KnownAnnTag FieldTag where
  annPrefix = "%"
instance KnownAnnTag VarTag where
  annPrefix = "@"
instance KnownAnnTag TypeTag where
  annPrefix = ":"
instance KnownAnnTag RootTag where
  annPrefix = "%"

instance KnownAnnTag tag => RenderDoc (Annotation tag) where
  renderDoc _ = renderAnn

instance KnownAnnTag tag => Buildable (Annotation tag) where
  build = buildRenderDoc

instance Show AnnotationSet where
  show = printDocS True . renderDoc doesntNeedParens

instance RenderDoc AnnotationSet where
  renderDoc _ (AnnotationSet {..}) =
    renderAnnGroup asTypes <+> renderAnnGroup asFields <+> renderAnnGroup asVars

instance Buildable AnnotationSet where
  build = buildRenderDoc

-- | Renders a single 'Annotation', this is used in every rendering instance of it.
-- Note that this also renders empty ones/'noAnn's because a single 'Annotation'
-- does not have enough context to know if it can be omitted, use 'singleAnnSet'
-- if you want to hide it instead.
renderAnn :: forall tag. KnownAnnTag tag => Annotation tag -> Doc
renderAnn (Annotation text) = textStrict $ annPrefix @tag <> text

-- | Renders a list of 'Annotation's, omitting unnecessary empty ones/'noAnn'.
-- This is used (3 times) to render an 'AnnotationSet'.
renderAnnGroup :: KnownAnnTag tag => [Annotation tag] -> Doc
renderAnnGroup = hsep . map renderAnn . trimEndNoAnn

--------------------------------------------------------------------------------
-- Creation and conversions
--------------------------------------------------------------------------------

noAnn :: Annotation a
noAnn = AnnotationUnsafe ""

-- | Makes an `Annotation` from its textual value, prefix (%/@/:) excluded
-- Throws an error if the given `Text` contains invalid characters
ann :: HasCallStack => Text -> Annotation a
ann = either error id . mkAnnotation

-- | Makes an `Annotation` from its textual value, prefix (%/@/:) excluded
-- Returns a `Text` error message if the given `Text` contains invalid characters
mkAnnotation :: Text -> Either Text (Annotation a)
mkAnnotation text
  -- TODO [#48] these are special annotations and should not be always allowed
  | text `elem` specialVarAnns = Right $ AnnotationUnsafe text
  | text == specialFieldAnn = Right $ AnnotationUnsafe text
  | otherwise = do
    suffix <- case T.uncons text of
      Just (h, tl) | isValidAnnStart h -> Right tl
      Just (h, _) -> Left $ T.snoc "Invalid first character: " h
      _ -> Right ""
    maybe (Right $ AnnotationUnsafe text) (Left . T.snoc "Invalid character: ") $
      T.find (not . isValidAnnBodyChar) suffix

-- | List of all the special Variable Annotations, only allowed in `CAR` and `CDR`
-- instructions, prefix (@) excluded.
-- These do not respect the rules of `isValidAnnStart` and `isValidAnnBodyChar`.
specialVarAnns :: [Text]
specialVarAnns = ["%%","%"]

-- | The only special Field Annotation, only allowed in `PAIR`, `LEFT` and
-- `RIGHT` instructions, prefix (%) excluded.
-- This does not respect the rules of `isValidAnnStart` and `isValidAnnBodyChar`.
specialFieldAnn :: Text
specialFieldAnn = "@"


-- | Checks if a `Char` is valid to be the first of an annotation, prefix
-- (%/@/:) excluded, the ones following should be checked with
-- `isValidAnnBodyChar` instead.
-- Note that this does not check Special Annotations, see `specialVarAnns`
-- and `specialFieldAnn`
isValidAnnStart :: Char -> Bool
isValidAnnStart x = (isAscii x && isAlpha x) || x == '_'

-- | Checks if a `Char` is valid to be part of an annotation, following a valid
-- first character (see `isValidAnnStart`) and the prefix (%/@/:).
-- Note that this does not check Special Annotations, see `specialVarAnns`
-- and `specialFieldAnn`
isValidAnnBodyChar :: Char -> Bool
isValidAnnBodyChar x =
  isValidAnnStart x || (isAscii x && isNumber x) ||  x `elem` (".%@" :: String)

instance Semigroup VarAnn where
  Annotation a <> Annotation b
    | a == "" || b == "" = ann $ a <> b
    | otherwise          = ann $ a <> "." <> b

instance Monoid VarAnn where
    mempty = noAnn

unifyAnn :: Annotation tag -> Annotation tag -> Maybe (Annotation tag)
unifyAnn (Annotation ann1) (Annotation ann2)
  | ann1 == "" || ann2 == "" = Just $ ann $ ann1 <> ann2
  | ann1 == ann2 = Just $ ann ann1
  | otherwise  = Nothing

ifAnnUnified :: Annotation tag -> Annotation tag -> Bool
ifAnnUnified a1 a2 = isJust $ a1 `unifyAnn` a2

disjoinVn :: VarAnn -> (VarAnn, VarAnn)
disjoinVn (Annotation a) = case T.findIndex (== '.') $ T.reverse a of
  Just ((n - 1 -) -> pos) -> (ann $ T.take pos a, ann $ T.drop (pos + 1) a)
  Nothing                 -> (noAnn, ann a)
  where
    n = T.length a

convAnn :: Annotation tag1 -> Annotation tag2
convAnn (Annotation a) = ann a

pattern WithAnn :: Annotation tag -> Annotation tag
pattern WithAnn ann <- ann@(Annotation (toString -> _:_))

deriveJSON morleyAesonOptions ''Annotation
deriveLift ''Annotation