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