morley-1.7.1: src/Michelson/FailPattern.hs
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
-- | Typical usages of FAILWITH instruction.
module Michelson.FailPattern
( TypicalFailWith
, typicalFailWithTag
, isTypicalFailWith
, modifyTypicalFailWith
, ConstantScope'
) where
import Michelson.Text (MText)
import Michelson.Typed
-- | We need this class to pass it to 'SomeConstrainedValue'.
-- 'ConstantScope' is needed because we push a value and 'Typeable' is
-- needed for 'FAILWITH'.
class (Typeable a, ConstantScope a) => ConstantScope' a
instance (Typeable a, ConstantScope a) => ConstantScope' a
-- | This data type captures typical ways to use `FAILWITH` instruction.
-- Each constructor corresponds to a usage pattern.
data TypicalFailWith
= FailWithString MText
-- ^ Push a constant string and fail with it.
| FailWithConstantPair MText (SomeConstrainedValue ConstantScope')
-- ^ Push a constant pair where the first item is a string and the
-- second one is an arbitrary constant. Fail afterwards.
| FailWithStackValue MText
-- ^ Push a constant string and apply the 'PAIR' instruction, then fail.
-- | Extract error tag out of 'TypicalFailWith'.
typicalFailWithTag :: TypicalFailWith -> MText
typicalFailWithTag = \case
FailWithString str -> str
FailWithConstantPair str _ -> str
FailWithStackValue str -> str
-- | Check whether given instruction ends with a typical 'FAILWITH'
-- usage. It does not recursively check instructions that can be
-- passed to other instructions.
--
-- The instruction MUST be linearized to the left (see 'linearizeLeft').
--
-- *IMPORTANT* be careful if you want to rewrite this function.
-- It took GHC about 30 seconds to compile this function previously.
-- The clause @isTypicalPreFailWith (Seq x PAIR) = ..@ is crucial, so please
-- don't move these two case branches in this clause outside of it:
-- either in different patten matching or in case or \case clause,
-- I usually experienced a regression for any of these options.
isTypicalFailWith :: Instr inp out -> Maybe TypicalFailWith
isTypicalFailWith (Seq i1 FAILWITH) = isTypicalPreFailWith i1
where
isTypicalPreFailWith :: Instr inp (a ': out) -> Maybe TypicalFailWith
isTypicalPreFailWith (Seq x PAIR) = case x of
Seq _ (PUSH v) -> FailWithStackValue <$> isStringValue v
PUSH v -> FailWithStackValue <$> isStringValue v
_ -> Nothing
isTypicalPreFailWith (Seq _ (PUSH v)) = isTypicalErrorConstant v
isTypicalPreFailWith (PUSH v) = isTypicalErrorConstant v
isTypicalPreFailWith _ = Nothing
isTypicalFailWith _ = Nothing
isTypicalErrorConstant ::
forall t. ConstantScope t => Value t -> Maybe TypicalFailWith
isTypicalErrorConstant v
| Just str <- isStringValue v = Just (FailWithString str)
| VPair (VString str, secondItem) <- v =
-- We need to pattern match to deduce `singI` for second item of the pair.
case sing @t of
STPair {} -> Just (FailWithConstantPair str (SomeConstrainedValue secondItem))
| otherwise = Nothing
-- | If given instruction ends with a typical 'FAILWITH' usage, modify
-- the tag used there using given transformation function. It can
-- return any value, not necessarily a string.
modifyTypicalFailWith
:: HasCallStack
=> (MText -> SomeConstrainedValue ConstantScope')
-> Instr inp out
-> Instr inp out
modifyTypicalFailWith f = modifyTypicalFailWith' . linearizeLeft
where
modifyTypicalFailWith' :: HasCallStack => Instr inp out -> Instr inp out
modifyTypicalFailWith' =
\case
Seq i1 FAILWITH ->
case i1 of
PUSH v -> onPush v
Seq i0 (PUSH v) -> Seq i0 (onPush v)
-- It is quite hard to move the body of these very similar
-- cases into a separate function because involved types
-- are quite sophisticated.
Seq (PUSH v) PAIR
| _ :: Value a <- v
, _ :: Instr (b ': s) ('TPair a b ': s) <- i1 ->
case sing @('TPair a b) of
STPair {} -> case isStringValue v of
Just (f -> SomeConstrainedValue v') ->
PUSH v' `Seq` PAIR `Seq` FAILWITH
Nothing ->
PUSH v `Seq` PAIR `Seq` FAILWITH
Seq (Seq i0 (PUSH v)) PAIR
| _ :: Value a <- v
, _ :: Instr s0 ('TPair a b ': s) <- i1 ->
case sing @('TPair a b) of
STPair {} -> Seq i0 $ case isStringValue v of
Just (f -> SomeConstrainedValue v') ->
PUSH v' `Seq` PAIR `Seq` FAILWITH
Nothing ->
PUSH v `Seq` PAIR `Seq` FAILWITH
_ -> Seq i1 FAILWITH
i -> i
onPush ::
(HasCallStack, Typeable v, ConstantScope v) => Value v -> Instr inp out
onPush v = case isTypicalErrorConstant v of
Just (FailWithString (f -> SomeConstrainedValue v')) -> PUSH v' `Seq` FAILWITH
Just (FailWithConstantPair (f -> SomeConstrainedValue v') (SomeConstrainedValue arg)) ->
PUSH (VPair (v', arg)) `Seq` FAILWITH
Just _ -> error "Unexpected TypicalFailWith"
Nothing -> PUSH v `Seq` FAILWITH