packages feed

morley-1.5.0: src/Michelson/Untyped/Contract.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

-- | Michelson contract in untyped model.

module Michelson.Untyped.Contract
  ( EntriesOrder (..)
  , canonicalEntriesOrder
  , entriesOrderToInt
  , mapEntriesOrdered

  , ContractBlock (..)
  , orderContractBlock

  , Contract' (..)
  , Storage
  ) where

import Data.Aeson.TH (deriveJSON)
import Data.Data (Data(..))
import Data.Default (Default(..))
import Fmt (Buildable(build))
import Text.PrettyPrint.Leijen.Text (nest, semi, text, (<$$>), (<+>))

import Michelson.Printer.Util
  (Prettier(..), RenderDoc(..), assertParensNotNeeded, buildRenderDoc, needsParens, renderOpsList)
import Michelson.Untyped.Type (ParameterType(..), Type(..))
import Util.Aeson

-- | Top-level entries order of the contract.
-- This is preserved due to the fact that it affects
-- the output of pretty-printing and serializing contract.

-- Each constructors is created by the order of the first letter of
-- @parameter@, @storage@, and @code@.
--
-- For example, @PSC@ would be @parameter@, @storage@ and @code@,
-- @CPS@ would be @code@, @parameter@,and @storage@, and so on.
data EntriesOrder
  = PSC
  | PCS
  | SPC
  | SCP
  | CSP
  | CPS
  deriving stock (Bounded, Data, Enum, Eq, Generic, Show)

instance Default EntriesOrder where
  def = canonicalEntriesOrder
instance NFData EntriesOrder

-- | The canonical entries order which is ordered as follow:
-- @parameter@, @storage@, and @code@.
canonicalEntriesOrder :: EntriesOrder
canonicalEntriesOrder = PSC

-- | @(Int, Int, Int)@ is the positions of @parameter@, @storage@, and @code@
-- respectively.
entriesOrderToInt :: EntriesOrder -> (Int, Int, Int)
entriesOrderToInt = \case
  PSC -> (0, 1, 2)
  PCS -> (0, 2, 1)
  SPC -> (1, 0, 2)
  SCP -> (1, 2, 0)
  CSP -> (2, 1, 0)
  CPS -> (2, 0, 1)

-- | Contract block, convenient when parsing
data ContractBlock op
  = CBParam ParameterType
  | CBStorage Type
  | CBCode [op]
  deriving stock (Eq, Show)

orderContractBlock :: (ContractBlock op, ContractBlock op, ContractBlock op) -> Maybe (Contract' op)
orderContractBlock = \case
  (CBParam   p, CBStorage s, CBCode    c) -> Just $ Contract p s c PSC
  (CBParam   p, CBCode    c, CBStorage s) -> Just $ Contract p s c PCS
  (CBStorage s, CBParam   p, CBCode    c) -> Just $ Contract p s c SPC
  (CBStorage s, CBCode    c, CBParam   p) -> Just $ Contract p s c SCP
  (CBCode    c, CBStorage s, CBParam   p) -> Just $ Contract p s c CSP
  (CBCode    c, CBParam   p, CBStorage s) -> Just $ Contract p s c CPS
  _                                       -> Nothing

-- | Map each contract fields by the given function and sort the output
-- based on the 'EntriesOrder'.
mapEntriesOrdered
  :: Contract' op
  -> (ParameterType -> a)
  -> (Storage -> a)
  -> ([op] -> a)
  -> [a]
mapEntriesOrdered Contract{..} fParam fStorage fCode =
  fmap snd
    $ sortWith fst
        [ (paramPos, fParam contractParameter)
        , (storagePos, fStorage contractStorage)
        , (codePos, fCode contractCode)
        ]
  where
    (paramPos, storagePos, codePos) = entriesOrderToInt entriesOrder

type Storage = Type
data Contract' op = Contract
  { contractParameter :: ParameterType
  , contractStorage :: Storage
  , contractCode :: [op]
  , entriesOrder :: EntriesOrder
  } deriving stock (Eq, Show, Functor, Data, Generic)

instance NFData op => NFData (Contract' op)

instance (RenderDoc op) => RenderDoc (Contract' op) where
  renderDoc pn contract =
    assertParensNotNeeded pn
      $ foldr (<$$>) (text "")
      $ mapEntriesOrdered contract
        (\parameter -> "parameter" <+> renderDoc needsParens (Prettier parameter) <> semi)
        (\storage -> "storage" <+> renderDoc needsParens (Prettier storage) <> semi)
        (\code -> "code" <+> nest (length ("code {" :: Text)) (renderOpsList False code <> semi))

instance RenderDoc op => Buildable (Contract' op) where
  build = buildRenderDoc

deriveJSON morleyAesonOptions ''EntriesOrder
deriveJSON morleyAesonOptions ''Contract'