packages feed

morley-1.15.0: src/Michelson/Typed/Util.hs

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

-- | General-purpose utility functions for typed types.

module Michelson.Typed.Util
  ( DfsSettings (..)
  , CtorEffectsApp (..)
  , ceaBottomToTop
  , dfsInstr
  , dfsFoldInstr
  , dfsModifyInstr

  -- * Changing instruction tree structure
  , linearizeLeft
  , linearizeLeftDeep

  -- * Value analysis
  , dfsFoldMapValue
  , dfsFoldMapValueM
  , dfsMapValue
  , dfsTraverseValue
  , isStringValue
  , isBytesValue
  , allAtomicValues

  -- * Instruction generation
  , PushableStorageSplit (..)
  , splitPushableStorage
  ) where

import Prelude hiding (Ordering(..))

import Control.Monad.Writer.Strict (execWriterT, runWriter, tell, writer)
import Data.Constraint (Dict(..))
import Data.Default (Default(..))
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Text.Show

import Michelson.Text (MText)
import Michelson.Typed.Aliases
import Michelson.Typed.Instr
import Michelson.Typed.Scope
import qualified Michelson.Typed.T as T
import Michelson.Typed.Value

-- | Options for 'dfsInstr'.
data DfsSettings x = DfsSettings
  { dsGoToValues :: Bool
    -- ^ Whether 'dfsInstr' function should go into values which contain other
    -- instructions: lambdas and constant contracts
    -- (which can be passed to @CREATE_CONTRACT@).
  , dsCtorEffectsApp :: CtorEffectsApp x
    -- ^ How do we handle intermediate nodes in instruction tree.
  } deriving stock (Show)

-- | Describes how intermediate nodes in instruction tree are accounted.
data CtorEffectsApp x = CtorEffectsApp
  { ceaName :: Text
    -- ^ Name of this way.
  , ceaApplyEffects
      :: forall i o. Semigroup x => x -> x -> Instr i o -> (Instr i o, x)
    -- ^ This function accepts:
    -- 1. Effects gathered after applying @step@ to node's children, but
    -- before applying it to the node itself.
    -- 2. Effects gathered after applying @step@ to the given intermediate node.
    -- 3. Instruction resulting after all modifications produced by @step@.
  }

instance Show (CtorEffectsApp x) where
  show CtorEffectsApp{..} = show ceaName

-- | Gather effects first for children nodes, then for their parents.
ceaBottomToTop :: CtorEffectsApp x
ceaBottomToTop = CtorEffectsApp
  { ceaName = "Apply after"
  , ceaApplyEffects =
      \effBefore effAfter instr -> (instr, effBefore <> effAfter)
  }

instance Default (DfsSettings x) where
  def = DfsSettings
    { dsGoToValues = False
    , dsCtorEffectsApp = ceaBottomToTop
    }

-- | Traverse a typed instruction in depth-first order.
-- '<>' is used to concatenate intermediate results.
-- Each instructions can be changed using the supplied @step@ function.
-- It does not consider extra instructions (not present in Michelson).
dfsInstr ::
     forall x inp out. Semigroup x
  => DfsSettings x
  -> (forall i o. Instr i o -> (Instr i o, x))
  -> Instr inp out
  -> (Instr inp out, x)
dfsInstr settings@DfsSettings{..} step i =
  case i of
    Seq i1 i2 -> recursion2 Seq i1 i2
    WithLoc loc i1 -> recursion1 (WithLoc loc) i1
    InstrWithNotes p notes i1 -> recursion1 (InstrWithNotes p notes) i1
    InstrWithVarNotes varNotes i1 -> recursion1 (InstrWithVarNotes varNotes) i1
    InstrWithVarAnns varAnns i1 -> recursion1 (InstrWithVarAnns varAnns) i1
    FrameInstr p i1 -> recursion1 (FrameInstr p) i1
    Nested i1 -> recursion1 Nested i1
    DocGroup dg i1 -> recursion1 (DocGroup dg) i1
    Fn t sfn i1 -> recursion1 (Fn t sfn) i1
    IF_NONE i1 i2 -> recursion2 IF_NONE i1 i2
    IF_LEFT i1 i2 -> recursion2 IF_LEFT i1 i2
    IF_CONS i1 i2 -> recursion2 IF_CONS i1 i2
    IF i1 i2 -> recursion2 IF i1 i2
    MAP i1 -> recursion1 MAP i1
    ITER i1 -> recursion1 ITER i1
    LOOP i1 -> recursion1 LOOP i1
    LOOP_LEFT i1 -> recursion1 LOOP_LEFT i1
    DIP i1 -> recursion1 DIP i1
    DIPN s i1 -> recursion1 (DIPN s) i1

    -- This case is more complex so we duplicate @recursion1@ a bit.
    -- We have to traverse the pushed value because a lambda can be
    -- somewhere inside of it (e. g. one item of a pair).
    PUSH v -> fromMaybe (step i) do
        guard dsGoToValues
        let
          valueStep :: forall t . Value t -> (Value t, Maybe x)
          valueStep = \case
            -- Using 'analyzeInstrFailure' here (and in case below) is cheap
            -- (O(n) in total) because we never make it run over the same code twice
            VLam lambda -> bimap (VLam . analyzeInstrFailure) Just $
              dfsInstr settings step (rfAnyInstr lambda)
            otherV -> (otherV, Nothing)
        -- Note that @dfsTraverseValue@ does not respect 'CtorEffectsApp',
        -- so if we encounter a value that contains more than one lambda
        -- this function may misbehave.
        -- That's very unlikely in practice.
        -- In #264 we will support this feature in @dfsTraverseValue@.
        let
          (innerV, innerXMaybe) = runWriter $ dfsTraverseValue (writer . valueStep) v

        innerX <- innerXMaybe
        let (outerI, outerX) = step $ PUSH innerV
        pure $ ceaApplyEffects dsCtorEffectsApp innerX outerX outerI

    LAMBDA (VLam i1)
      | dsGoToValues ->
          recursion1 (LAMBDA . VLam . analyzeInstrFailure) (rfAnyInstr i1)
      | otherwise -> step i
    CREATE_CONTRACT contract
      | dsGoToValues ->
        let updateContractCode code = CREATE_CONTRACT $ contract{ cCode = code }
        in recursion1 updateContractCode $ cCode contract
      | otherwise -> step i

    Nop{} -> step i
    Ext (TEST_ASSERT (TestAssert nm pc i1)) ->
      recursion1 (Ext . TEST_ASSERT . TestAssert nm pc) i1
    Ext{} -> step i
    AnnCAR{} -> step i
    AnnCDR{} -> step i
    DROP{} -> step i
    DROPN{} -> step i
    DUP{} -> step i
    DUPN{} -> step i
    SWAP{} -> step i
    DIG{} -> step i
    DUG{} -> step i
    SOME{} -> step i
    NONE{} -> step i
    UNIT{} -> step i
    AnnPAIR{} -> step i
    AnnUNPAIR{} -> step i
    PAIRN{} -> step i
    UNPAIRN{} -> step i
    AnnLEFT{} -> step i
    AnnRIGHT{} -> step i
    NIL{} -> step i
    CONS{} -> step i
    SIZE{} -> step i
    EMPTY_SET{} -> step i
    EMPTY_MAP{} -> step i
    EMPTY_BIG_MAP{} -> step i
    MEM{} -> step i
    GET{} -> step i
    GETN{} -> step i
    UPDATE{} -> step i
    UPDATEN{} -> step i
    GET_AND_UPDATE{} -> step i
    EXEC{} -> step i
    APPLY{} -> step i
    FAILWITH{} -> step i
    CAST{} -> step i
    RENAME{} -> step i
    PACK{} -> step i
    UNPACK{} -> step i
    CONCAT{} -> step i
    CONCAT'{} -> step i
    SLICE{} -> step i
    ISNAT{} -> step i
    ADD{} -> step i
    SUB{} -> step i
    MUL{} -> step i
    EDIV{} -> step i
    ABS{} -> step i
    NEG{} -> step i
    LSL{} -> step i
    LSR{} -> step i
    OR{} -> step i
    AND{} -> step i
    XOR{} -> step i
    NOT{} -> step i
    COMPARE{} -> step i
    EQ{} -> step i
    NEQ{} -> step i
    LT{} -> step i
    GT{} -> step i
    LE{} -> step i
    GE{} -> step i
    INT{} -> step i
    SELF{} -> step i
    CONTRACT{} -> step i
    TRANSFER_TOKENS{} -> step i
    SET_DELEGATE{} -> step i
    IMPLICIT_ACCOUNT{} -> step i
    NOW{} -> step i
    AMOUNT{} -> step i
    BALANCE{} -> step i
    VOTING_POWER{} -> step i
    TOTAL_VOTING_POWER{} -> step i
    CHECK_SIGNATURE{} -> step i
    SHA256{} -> step i
    SHA512{} -> step i
    BLAKE2B{} -> step i
    SHA3{} -> step i
    KECCAK{} -> step i
    HASH_KEY{} -> step i
    PAIRING_CHECK{} -> step i
    SOURCE{} -> step i
    SENDER{} -> step i
    ADDRESS{} -> step i
    CHAIN_ID{} -> step i
    LEVEL{} -> step i
    SELF_ADDRESS{} -> step i
    NEVER{} -> step i
    TICKET{} -> step i
    READ_TICKET{} -> step i
    SPLIT_TICKET{} -> step i
    JOIN_TICKETS{} -> step i
  where
    recursion1 ::
      forall a b c d. (Instr a b -> Instr c d) -> Instr a b -> (Instr c d, x)
    recursion1 constructor i0 =
      let
        (innerI, innerX) = dfsInstr settings step i0
        (outerI, outerX) = step $ constructor innerI
      in ceaApplyEffects dsCtorEffectsApp innerX outerX outerI

    recursion2 ::
      forall i o i1 o1 i2 o2.
      (Instr i1 o1 -> Instr i2 o2 -> Instr i o) ->
      Instr i1 o1 -> Instr i2 o2 -> (Instr i o, x)
    recursion2 constructor i1 i2 =
      let
        (i1', x1) = dfsInstr settings step i1
        (i2', x2) = dfsInstr settings step i2
        (i', x) = step $ constructor i1' i2'
      in ceaApplyEffects dsCtorEffectsApp (x1 <> x2) x i'

-- | Specialization of 'dfsInstr' for case when changing the instruction is
-- not required.
dfsFoldInstr
  :: forall x inp out.
      (Semigroup x)
  => DfsSettings x
  -> (forall i o. Instr i o -> x)
  -> Instr inp out
  -> x
dfsFoldInstr settings step instr =
  snd $ dfsInstr settings (\i -> (i, step i)) instr

-- | Specialization of 'dfsInstr' which only modifies given instruction.
dfsModifyInstr
  :: DfsSettings ()
  -> (forall i o. Instr i o -> Instr i o)
  -> Instr inp out
  -> Instr inp out
dfsModifyInstr settings step instr =
  fst $ dfsInstr settings (\i -> (step i, ())) instr

-- | Check whether instruction fails at each execution path or have at least one
-- non-failing path.
--
-- This function assumes that given instruction contains no dead code
-- (contract with dead code cannot be valid Michelson contract) and may behave
-- in unexpected way if such is present. Term "dead code" includes instructions
-- which render into empty Michelson, like Morley extensions.
-- On the other hand, this function does not traverse the whole instruction tree;
-- performs fastest on left-growing combs.
--
-- Often we already have information about instruction failure, use this
-- function only in cases when this info is actually unavailable or hard
-- to use.
analyzeInstrFailure :: HasCallStack => Instr i o -> RemFail Instr i o
analyzeInstrFailure = go
  where
  go :: Instr i o -> RemFail Instr i o
  go = \case
    WithLoc loc i -> case go i of
      RfNormal i0 ->
        RfNormal (WithLoc loc i0)
      r -> r
    InstrWithNotes p pn i -> case go i of
      RfNormal i0 ->
        RfNormal (InstrWithNotes p pn i0)
      RfAlwaysFails i0 ->
        error $ "InstrWithNotes wraps always-failing instruction: " <> show i0
    InstrWithVarNotes vn i -> case go i of
      RfNormal i0 ->
        RfNormal (InstrWithVarNotes vn i0)
      RfAlwaysFails i0 ->
        error $ "InstrWithVarNotes wraps always-failing instruction: " <> show i0
    InstrWithVarAnns vn i -> case go i of
      RfNormal i0 ->
        RfNormal (InstrWithVarAnns vn i0)
      RfAlwaysFails i0 ->
        error $ "InstrWithVarAnns wraps always-failing instruction: " <> show i0
    FrameInstr s i -> case go i of
      RfNormal i0 ->
        RfNormal (FrameInstr s i0)
      RfAlwaysFails i0 ->
        error $ "FrameInstr wraps always-failing instruction: " <> show i0
    Seq a b -> Seq a `rfMapAnyInstr` go b
    Nop -> RfNormal Nop
    Ext e -> RfNormal (Ext e)
    Nested i -> Nested `rfMapAnyInstr` go i
    DocGroup g i -> DocGroup g `rfMapAnyInstr` go i
    Fn t sfn i -> Fn t sfn `rfMapAnyInstr` go i

    IF_NONE l r -> rfMerge IF_NONE (go l) (go r)
    IF_LEFT l r -> rfMerge IF_LEFT (go l) (go r)
    IF_CONS l r -> rfMerge IF_CONS (go l) (go r)
    IF l r -> rfMerge IF (go l) (go r)

    i@MAP{} -> RfNormal i
    i@ITER{} -> RfNormal i
    i@LOOP{} -> RfNormal i
    i@LOOP_LEFT{} -> RfNormal i
    i@LAMBDA{} -> RfNormal i
    i@DIP{} -> RfNormal i
    i@DIPN{} -> RfNormal i

    i@AnnCAR{} -> RfNormal i
    i@AnnCDR{} -> RfNormal i
    i@DROP{} -> RfNormal i
    i@DROPN{} -> RfNormal i
    i@DUP{} -> RfNormal i
    i@DUPN{} -> RfNormal i
    i@SWAP{} -> RfNormal i
    i@DIG{} -> RfNormal i
    i@DUG{} -> RfNormal i
    i@PUSH{} -> RfNormal i
    i@SOME{} -> RfNormal i
    i@NONE{} -> RfNormal i
    i@UNIT{} -> RfNormal i
    i@AnnPAIR{} -> RfNormal i
    i@AnnUNPAIR{} -> RfNormal i
    i@PAIRN{} -> RfNormal i
    i@UNPAIRN{} -> RfNormal i
    i@AnnLEFT{} -> RfNormal i
    i@AnnRIGHT{} -> RfNormal i
    i@NIL{} -> RfNormal i
    i@CONS{} -> RfNormal i
    i@SIZE{} -> RfNormal i
    i@EMPTY_SET{} -> RfNormal i
    i@EMPTY_MAP{} -> RfNormal i
    i@EMPTY_BIG_MAP{} -> RfNormal i
    i@MEM{} -> RfNormal i
    i@GET{} -> RfNormal i
    i@GETN{} -> RfNormal i
    i@UPDATE{} -> RfNormal i
    i@UPDATEN{} -> RfNormal i
    i@GET_AND_UPDATE{} -> RfNormal i
    i@EXEC{} -> RfNormal i
    i@APPLY{} -> RfNormal i
    FAILWITH -> RfAlwaysFails FAILWITH
    i@CAST -> RfNormal i
    i@RENAME -> RfNormal i
    i@PACK -> RfNormal i
    i@UNPACK -> RfNormal i
    i@CONCAT -> RfNormal i
    i@CONCAT' -> RfNormal i
    i@SLICE -> RfNormal i
    i@ISNAT -> RfNormal i
    i@ADD -> RfNormal i
    i@SUB -> RfNormal i
    i@MUL -> RfNormal i
    i@EDIV -> RfNormal i
    i@ABS -> RfNormal i
    i@NEG -> RfNormal i
    i@LSL -> RfNormal i
    i@LSR -> RfNormal i
    i@OR -> RfNormal i
    i@AND -> RfNormal i
    i@XOR -> RfNormal i
    i@NOT -> RfNormal i
    i@COMPARE -> RfNormal i
    i@EQ -> RfNormal i
    i@NEQ -> RfNormal i
    i@LT -> RfNormal i
    i@GT -> RfNormal i
    i@LE -> RfNormal i
    i@GE -> RfNormal i
    i@INT -> RfNormal i
    i@SELF{} -> RfNormal i
    i@CONTRACT{} -> RfNormal i
    i@TRANSFER_TOKENS -> RfNormal i
    i@SET_DELEGATE -> RfNormal i
    i@CREATE_CONTRACT{} -> RfNormal i
    i@IMPLICIT_ACCOUNT -> RfNormal i
    i@NOW -> RfNormal i
    i@AMOUNT -> RfNormal i
    i@BALANCE -> RfNormal i
    i@VOTING_POWER -> RfNormal i
    i@TOTAL_VOTING_POWER -> RfNormal i
    i@CHECK_SIGNATURE -> RfNormal i
    i@SHA256 -> RfNormal i
    i@SHA512 -> RfNormal i
    i@BLAKE2B -> RfNormal i
    i@SHA3 -> RfNormal i
    i@KECCAK -> RfNormal i
    i@HASH_KEY -> RfNormal i
    i@PAIRING_CHECK -> RfNormal i
    i@SOURCE -> RfNormal i
    i@SENDER -> RfNormal i
    i@ADDRESS -> RfNormal i
    i@CHAIN_ID -> RfNormal i
    i@LEVEL -> RfNormal i
    i@SELF_ADDRESS -> RfNormal i
    NEVER -> RfAlwaysFails NEVER
    i@TICKET -> RfNormal i
    i@READ_TICKET -> RfNormal i
    i@SPLIT_TICKET -> RfNormal i
    i@JOIN_TICKETS -> RfNormal i

-- | There are many ways to represent a sequence of more than 2 instructions.
-- E. g. for @i1; i2; i3@ it can be @Seq i1 $ Seq i2 i3@ or @Seq (Seq i1 i2) i3@.
-- This function enforces a particular structure. Specifically, it makes each
-- 'Seq' have a single instruction (i. e. not 'Seq') in its second argument.
-- This function also erases redundant 'Nop's.
--
-- Please note that this function is not recursive, it does not
-- linearize contents of @IF@ and similar instructions.
linearizeLeft :: Instr inp out -> Instr inp out
linearizeLeft = linearizeLeftHelper False
  where
    -- In order to avoid quadratic performance we make a simple optimization.
    -- We track whether left argument of `Seq` is already linearized.
    -- If it is, we do not need to ever linearize it again.
    linearizeLeftHelper :: Bool -> Instr inp out -> Instr inp out
    linearizeLeftHelper isLeftInstrAlreadyLinear =
      \case
        Seq i1 (Seq i2 i3) ->
          linearizeLeftHelper True $
          Seq (linearizeLeftHelper isLeftInstrAlreadyLinear (Seq i1 i2)) i3
        -- `i2` is not a `Seq`, so we only need to linearize `i1`
        -- and connect it with `i2`.
        Seq i1 i2
          | isLeftInstrAlreadyLinear
          , Nop <- i2 -> i1
          | isLeftInstrAlreadyLinear -> Seq i1 i2
          | Nop <- i2 -> linearizeLeft i1
          | otherwise -> Seq (linearizeLeft i1) i2
        i -> i

-- | "Deep" version of 'linearizeLeft'. It recursively linearizes
-- instructions stored in other instructions.
linearizeLeftDeep :: Instr inp out -> Instr inp out
linearizeLeftDeep = dfsModifyInstr def linearizeLeft

----------------------------------------------------------------------------
-- Value analysis
----------------------------------------------------------------------------

-- | Traverse a value in depth-first order.
dfsMapValue ::
     forall t.
     (forall t'. Value t' -> Value t')
  -> Value t
  -> Value t
dfsMapValue step v = runIdentity $ dfsTraverseValue (pure . step) v

-- | Traverse a value in depth-first order.
dfsTraverseValue ::
     forall t m.
     (Monad m)
  => (forall t'. Value t' -> m (Value t'))
  -> Value t
  -> m (Value t)
dfsTraverseValue step i = case i of
  -- Atomic
  VKey{} -> step i
  VUnit -> step i
  VSignature{} -> step i
  VChainId{} -> step i
  VOp{} -> step i
  VContract{} -> step i
  VTicket{} -> step i  -- cannot appear as constant in a contract
  VLam{} -> step i
  VInt{} -> step i
  VNat{} -> step i
  VString{} -> step i
  VBytes{} -> step i
  VMutez{} -> step i
  VBool{} -> step i
  VKeyHash{} -> step i
  VBls12381Fr{} -> step i
  VBls12381G1{} -> step i
  VBls12381G2{} -> step i
  VTimestamp{} -> step i
  VAddress{} -> step i

  -- Non-atomic
  VOption mVal -> case mVal of
    Nothing -> step i
    Just val -> recursion1 (VOption . Just) val
  VList vals -> do
    vs <- traverse (dfsTraverseValue step) vals
    step $ VList vs
  VSet vals -> do
    cs <- S.fromList <$> traverse (dfsTraverseValue step) (S.toList vals)
    step (VSet cs)
  VPair (v1, v2) -> do
    v1' <- dfsTraverseValue step v1
    v2' <- dfsTraverseValue step v2
    step $ VPair (v1', v2')
  VOr vEither -> case vEither of
    Left v -> recursion1 (VOr . Left) v
    Right v -> recursion1 (VOr . Right) v
  VMap vmap -> mapRecursion VMap vmap
  VBigMap bmId vmap -> mapRecursion (VBigMap bmId) vmap
  where
    recursion1 ::
         forall t'.
         (Value t' -> Value t)
      -> Value t'
      -> m (Value t)
    recursion1 constructor v = do
      v' <- dfsTraverseValue step v
      step $ constructor v'

    mapRecursion
      :: forall k v. Comparable k
      => (M.Map (Value k) (Value v) -> Value t)
      -> M.Map (Value k) (Value v)
      -> m (Value t)
    mapRecursion constructor vmap = do
      vmap' <-
        M.fromList <$> forM (M.toList vmap) \(k, v) -> do
          k' <- dfsTraverseValue step k
          v' <- dfsTraverseValue step v
          pure (k', v')
      step $ constructor vmap'

-- | Specialization of 'dfsMapValue' for case when changing the value is
-- not required.
dfsFoldMapValue ::
  Monoid x =>
  (forall t'. Value t' -> x)
  -> Value t
  -> x
dfsFoldMapValue step v =
  runIdentity $ dfsFoldMapValueM (pure . step) v

-- | Specialization of 'dfsMapValue' for case when changing the value is
-- not required.
dfsFoldMapValueM
  :: (Monoid x, Monad m)
  => (forall t'. Value t' -> m x)
  -> Value t
  -> m x
dfsFoldMapValueM step v = do
  execWriterT $
    dfsTraverseValue
      (\val -> do
          x <- lift $ step val
          tell x
          pure val
      )
      v

-- | If value is a string, return the stored string.
isStringValue :: Value t -> Maybe MText
isStringValue =
  \case
    VString str -> Just str
    _ -> Nothing

-- | If value is a bytestring, return the stored bytestring.
isBytesValue :: Value t -> Maybe ByteString
isBytesValue =
  \case
    VBytes bytes -> Just bytes
    _ -> Nothing

-- | Takes a selector which checks whether a value can be converted
-- to something. Recursively applies it to all values. Collects extracted
-- values in a list.
allAtomicValues ::
  forall t a. (forall t'. Value t' -> Maybe a) -> Value t -> [a]
allAtomicValues selector = dfsFoldMapValue (maybeToList . selector)


--------------------------------------------------------------------------------
-- Instruction generation
--------------------------------------------------------------------------------

-- | Result of splitting a storage 'Value' of @st@ on the stack @s@.
--
-- The idea behind this is to either: prove that the whole 'Value' can be put on
-- the stack without containing a single @big_map@ or to split it into:
-- a 'Value' containing its @big_map@s and an instruction to reconstruct the
-- storage.
--
-- The main idea behind this is to create a large storage in Michelson code to
-- then create a contract using 'CREATE_CONTRACT'.
-- Note: a simpler solution would have been to replace @big_map@ 'Value's with
-- an 'EMPTY_BIG_MAP' followed by many 'UPDATE' to push its content, but sadly
-- a bug (tezos/tezos/1154) prevents this from being done.
data PushableStorageSplit s st where
  -- | The type of the storage is fully constant.
  ConstantStorage
    :: ConstantScope st
    => Value st
    -> PushableStorageSplit s st

  -- | The type of the storage is not a constant, but its value does not contain
  -- @big_map@s. E.g. A 'Right ()' value of type 'Either (BigMap k v) ()'.
  PushableValueStorage
    :: StorageScope st
    => Instr s (st ': s)
    -> PushableStorageSplit s st

  -- | The type of the storage and part of its value (here @heavy@) contain one or
  -- more @big_map@s or @ticket@s. The instruction can take the non-pushable
  -- 'Value heavy' and reconstruct the original 'Value st' without using any
  -- 'EMPTY_BIG_MAP'.
  PartlyPushableStorage
    :: (StorageScope heavy, StorageScope st)
    => Value heavy -> Instr (heavy ': s) (st ': s)
    -> PushableStorageSplit s st

-- | Splits the given storage 'Value' into a 'PushableStorageSplit'.
--
-- This is based off the fact that the only storages that cannot be directly
-- 'PUSH'ed are the ones that contain 'BigMap's and tickets.
-- See difference between 'StorageScope' and 'ConstantScope'.
--
-- So what we do here is to create a 'Value' as small as possible with all the
-- @big_map@s in it (if any) and an 'Instr' that can use it to rebuild the original
-- storage 'Value'.
--
-- Note: This is done this way to avoid using 'EMPTY_BIG_MAP' instructions, see
-- 'PushableStorageSplit' for motivation.
splitPushableStorage :: StorageScope t => Value t -> PushableStorageSplit s t
splitPushableStorage v = case v of
  -- Atomic (except op and contract)
  VKey{}        -> ConstantStorage v
  VUnit         -> ConstantStorage v
  VSignature{}  -> ConstantStorage v
  VChainId{}    -> ConstantStorage v
  VLam{}        -> ConstantStorage v
  VInt{}        -> ConstantStorage v
  VNat{}        -> ConstantStorage v
  VString{}     -> ConstantStorage v
  VBytes{}      -> ConstantStorage v
  VMutez{}      -> ConstantStorage v
  VBool{}       -> ConstantStorage v
  VKeyHash{}    -> ConstantStorage v
  VBls12381Fr{} -> ConstantStorage v
  VBls12381G1{} -> ConstantStorage v
  VBls12381G2{} -> ConstantStorage v
  VTimestamp{}  -> ConstantStorage v
  VAddress{}    -> ConstantStorage v
  VTicket{}     -> PartlyPushableStorage v Nop

  -- Non-atomic
  VOption (Nothing :: Maybe (Value tm)) -> case checkScope @(ConstantScope tm) of
    Right Dict -> ConstantStorage $ VOption Nothing
    Left _     -> PushableValueStorage $ NONE
  VOption (Just jVal :: Maybe (Value tm)) -> case splitPushableStorage jVal of
    ConstantStorage _ -> ConstantStorage . VOption $ Just jVal
    PushableValueStorage instr -> PushableValueStorage $ instr `Seq` SOME
    PartlyPushableStorage val instr -> PartlyPushableStorage val $ instr `Seq` SOME

  VList (vals :: [Value tl]) -> case checkScope @(ConstantScope tl) of
    Right Dict -> ConstantStorage v
    Left _     ->
      -- Here we check that even tho the type contains big_maps, we actually
      -- have big_maps in (one or more of) the values too.
      let handleList
            :: Instr s ('T.TList tl ': s) -> Value tl
            -> Maybe (Instr s ('T.TList tl ': s))
          handleList instr ele = case splitPushableStorage ele of
            ConstantStorage val ->
              Just $ instr `Seq` PUSH val `Seq` CONS
            PushableValueStorage eleInstr ->
              Just $ instr `Seq` eleInstr `Seq` CONS
            PartlyPushableStorage _ _ -> Nothing
      in  maybe (PartlyPushableStorage v Nop) PushableValueStorage $
            foldM handleList NIL vals

  VSet{} -> ConstantStorage v

  VPair (v1 :: Value t1, v2 :: Value t2) ->
    withValueTypeSanity v1 $ withValueTypeSanity v2 $
      withDeMorganScope @StorageScope @'T.TPair @t1 @t2 $
        let handlePair
              :: PushableStorageSplit s t2
              -> PushableStorageSplit (t2 ': s) t1
              -> PushableStorageSplit s ('T.TPair t1 t2)
            handlePair psp2 psp1 = case (psp2, psp1) of
              -- at least one side is a constant
              (ConstantStorage _, ConstantStorage _) ->
                ConstantStorage v
              (ConstantStorage val2, _) ->
                handlePair (PushableValueStorage $ PUSH val2) psp1
              (_, ConstantStorage val1) ->
                handlePair psp2 (PushableValueStorage $ PUSH val1)
              -- at least one side is a constant or has no big_map values
              (PushableValueStorage instr2, PushableValueStorage instr1) ->
                PushableValueStorage $ instr2 `Seq` instr1 `Seq` PAIR
              (PushableValueStorage instr2, PartlyPushableStorage val1 instr1) ->
                PartlyPushableStorage val1 $ DIP instr2 `Seq` instr1 `Seq` PAIR
              (PartlyPushableStorage val2 instr2, PushableValueStorage instr1) ->
                PartlyPushableStorage val2 $ instr2 `Seq` instr1 `Seq` PAIR
              -- both sides contain a big_map
              (PartlyPushableStorage val2 instr2, PartlyPushableStorage val1 instr1) ->
                PartlyPushableStorage (VPair (val1, val2)) $
                  UNPAIR `Seq` DIP instr2 `Seq` instr1 `Seq` PAIR
        in handlePair (splitPushableStorage v2) (splitPushableStorage v1)

  VOr (Left orVal :: Either (Value t1) (Value t2)) ->
    withValueTypeSanity orVal $ withDeMorganScope @StorageScope @'T.TOr @t1 @t2 $
      case splitPushableStorage orVal of
        ConstantStorage val -> case checkScope @(ConstantScope t2) of
          -- note: here we need to check for the opposite branch too
          Right Dict -> ConstantStorage v
          Left _ -> PushableValueStorage $ PUSH val `Seq` LEFT
        PushableValueStorage instr -> PushableValueStorage $ instr `Seq` LEFT
        PartlyPushableStorage val instr -> PartlyPushableStorage val $ instr `Seq` LEFT
  VOr (Right orVal :: Either (Value t1) (Value t2)) ->
    withValueTypeSanity orVal $ withDeMorganScope @StorageScope @'T.TOr @t1 @t2 $
      case splitPushableStorage orVal of
        ConstantStorage val -> case checkScope @(ConstantScope t1) of
          -- note: here we need to check for the opposite branch too
          Right Dict -> ConstantStorage v
          Left _ -> PushableValueStorage $ PUSH val `Seq` RIGHT
        PushableValueStorage instr -> PushableValueStorage $ instr `Seq` RIGHT
        PartlyPushableStorage val instr -> PartlyPushableStorage val $ instr `Seq` RIGHT

  VMap (vMap :: (Map (Value tk) (Value tv))) -> case checkScope @(ConstantScope tk) of
    Left _ ->
      -- NOTE: all keys for a map need to be comparable and (even tho it's
      -- not a superclass) that means they have to be constants.
      -- I (@pasqu4le) found no exception to this rule, perhaps it should be
      -- enforced in the type system as well, but it may have more
      -- downsides than it's worth accepting.
      error "impossible: all map keys should be PUSHable"
    Right Dict -> case checkScope @(ConstantScope tv) of
      Right Dict -> ConstantStorage v
      _ -> withDeMorganScope @HasNoOp @'T.TMap @tk @tv $
        -- Similarly as for lists, here we check that even tho the value type
        -- contains a big_map, we actually have big_maps in (one or more of) them.
        let handleMap
              :: Instr s ('T.TMap tk tv ': s) -> (Value tk, Value tv)
              -> Maybe (Instr s ('T.TMap tk tv ': s))
            handleMap instr (key, ele) = case splitPushableStorage (VOption $ Just ele) of
              ConstantStorage val ->
                Just $ instr `Seq` PUSH val `Seq` PUSH key `Seq` UPDATE
              PushableValueStorage eleInstr ->
                Just $ instr `Seq` eleInstr `Seq` PUSH key `Seq` UPDATE
              PartlyPushableStorage _ _ -> Nothing
        in  maybe (PartlyPushableStorage v Nop) PushableValueStorage $
              foldM handleMap EMPTY_MAP $ M.toList vMap

  VBigMap _ _ -> PartlyPushableStorage v Nop