-- SPDX-FileCopyrightText: 2021 Oxhead Alpha -- SPDX-License-Identifier: LicenseRef-MIT-OA -- | General-purpose utility functions for typed types. module Morley.Michelson.Typed.Util ( -- * Instruction analysis DfsSettings (..) , CtorEffectsApp (..) , dfsTraverseInstr , dfsFoldInstr , dfsModifyInstr , isMichelsonInstr -- * Changing instruction tree structure , linearizeLeft , linearizeLeftDeep -- * Value analysis , dfsFoldMapValue , dfsFoldMapValueM , dfsMapValue , dfsTraverseValue , isStringValue , isBytesValue , allAtomicValues -- * Instruction generation , PushableStorageSplit (..) , splitPushableStorage -- * Working with 'RemFail' , analyzeInstrFailure -- * Annotations , SomeAnns(..) , instrAnns ) where import Prelude hiding (Ordering(..)) import Control.Monad.Writer.Strict (Writer, execWriterT, runWriter, tell, writer) import Data.Constraint (Dict(..), (\\)) import Data.Default (Default(..)) import Data.Generics (listify) import Data.Map qualified as M import Data.Set qualified as S import Fmt (Buildable(..)) import Language.Haskell.TH qualified as TH import Morley.Michelson.Text (MText) import Morley.Michelson.Typed.Aliases import Morley.Michelson.Typed.Annotation import Morley.Michelson.Typed.ClassifiedInstr import Morley.Michelson.Typed.Contract import Morley.Michelson.Typed.Instr import Morley.Michelson.Typed.Scope import Morley.Michelson.Typed.T qualified as T import Morley.Michelson.Typed.Value import Morley.Michelson.Typed.View import Morley.Michelson.Untyped (AnyAnn) -- $setup -- >>> :m + Morley.Michelson.Typed.Instr -- | Options for 'dfsTraverseInstr' family of functions. data DfsSettings m = DfsSettings { dsGoToValues :: Bool -- ^ Whether @dfsTraverseInstr@ function should go into values which contain other -- instructions: lambdas and constant contracts -- (which can be passed to @CREATE_CONTRACT@). , dsCtorEffectsApp :: CtorEffectsApp m -- ^ How do we handle intermediate nodes in instruction tree. , dsInstrStep :: (forall i o. Instr i o -> m (Instr i o)) , dsValueStep :: (forall t'. Value t' -> m (Value t')) } -- | Describes how intermediate nodes in instruction tree are accounted. data CtorEffectsApp m = CtorEffectsApp { ceaName :: Text -- ^ Name of this way. , ceaPostStep :: forall i o. Monad m => Instr i o -> m (Instr i o) -> m (Instr i o) -- ^ This transformation is applied after the step. -- It will be provided with the old instruction and the action gathered -- with the recursive traversal for the instruction subtree, -- and the result will go to the parent node. } instance Buildable (CtorEffectsApp x) where build CtorEffectsApp{..} = build ceaName instance (Applicative x) => Default (DfsSettings x) where def = DfsSettings { dsGoToValues = False , dsCtorEffectsApp = CtorEffectsApp { ceaName = "Do nothing" , ceaPostStep = const id } , dsInstrStep = pure , dsValueStep = pure } -- | Traverse a typed instruction in depth-first order. -- -- The 'dsInstrStep' and 'dsValueStep' actions will be applied in bottom-to-top order, i.e. -- first to the children of a node, then to the node itself. dfsTraverseInstr :: forall m inp out. (Monad m) => DfsSettings m -> Instr inp out -> m (Instr inp out) dfsTraverseInstr settings@DfsSettings{..} i = i & withClassifiedInstr \case SMayHaveChildren -> \case C_Ext (TEST_ASSERT (TestAssert nm pc i1)) -> recursion1 (Ext . TEST_ASSERT . TestAssert nm pc) i1 C_Ext _ -> recursion0 i STwoChildren -> \case C_Seq i1 i2 -> recursion2 Seq i1 i2 C_IF_NONE i1 i2 -> recursion2 IF_NONE i1 i2 C_IF_LEFT i1 i2 -> recursion2 IF_LEFT i1 i2 C_IF_CONS i1 i2 -> recursion2 IF_CONS i1 i2 C_IF i1 i2 -> recursion2 IF i1 i2 SOneChild -> \case C_WithLoc loc i1 -> recursion1 (WithLoc loc) i1 C_Meta meta i1 -> recursion1 (Meta meta) i1 C_Nested i1 -> recursion1 Nested i1 C_DocGroup dg i1 -> recursion1 (DocGroup dg) i1 C_AnnMAP ann i1 -> recursion1 (AnnMAP ann) i1 C_ITER i1 -> recursion1 ITER i1 C_LOOP i1 -> recursion1 LOOP i1 C_LOOP_LEFT i1 -> recursion1 LOOP_LEFT i1 C_DIP i1 -> recursion1 DIP i1 C_DIPN s i1 -> recursion1 (DIPN s) i1 SHasIndirectChildren | dsGoToValues -> \case -- 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). C_AnnPUSH ann v -> ceaPostStep dsCtorEffectsApp i do innerV <- dfsTraverseValue settings v dsInstrStep $ AnnPUSH ann innerV C_AnnLAMBDA ann i1 -> recursion1 (AnnLAMBDA ann . analyzeInstrFailure) (rfAnyInstr i1) C_AnnLAMBDA_REC ann i1 -> recursion1 (AnnLAMBDA_REC ann . analyzeInstrFailure) (rfAnyInstr i1) C_AnnCREATE_CONTRACT ann contract@Contract{..} -> ceaPostStep dsCtorEffectsApp i case cCode of ContractCode c -> do codeI <- dfsTraverseInstr settings c viewsI <- forM (unViewsSet cViews) \(SomeView v@View{..}) -> do code <- dfsTraverseInstr settings vCode pure $ SomeView v{ vCode = code } dsInstrStep $ AnnCREATE_CONTRACT ann $ contract { cCode = ContractCode codeI , cViews = ViewsSet viewsI } | otherwise -> const $ recursion0 i SNoChildren -> const $ recursion0 i where recursion0 :: forall a b. Instr a b -> m (Instr a b) recursion0 i0 = ceaPostStep dsCtorEffectsApp i0 $ dsInstrStep i0 recursion1 :: forall a b c d. (Instr a b -> Instr c d) -> Instr a b -> m (Instr c d) recursion1 constructor i0 = ceaPostStep dsCtorEffectsApp (constructor i0) do innerI <- dfsTraverseInstr settings i0 dsInstrStep $ constructor innerI recursion2 :: forall i o i1 o1 i2 o2. (Instr i1 o1 -> Instr i2 o2 -> Instr i o) -> Instr i1 o1 -> Instr i2 o2 -> m (Instr i o) recursion2 constructor i1 i2 = ceaPostStep dsCtorEffectsApp (constructor i1 i2) do i1' <- dfsTraverseInstr settings i1 i2' <- dfsTraverseInstr settings i2 dsInstrStep $ constructor i1' i2' -- | Specialization of 'dfsTraverseInstr' for case when changing the instruction is -- not required. dfsFoldInstr :: forall x inp out. (Monoid x) => DfsSettings (Writer x) -> (forall i o. Instr i o -> x) -> Instr inp out -> x dfsFoldInstr settings step = snd . runWriter . dfsTraverseInstr settings{dsInstrStep = writer . (id &&& step)} -- | Specialization of 'dfsTraverseInstr' which only modifies given instruction. dfsModifyInstr :: DfsSettings Identity -> (forall i o. Instr i o -> Instr i o) -> Instr inp out -> Instr inp out dfsModifyInstr settings step = runIdentity . dfsTraverseInstr settings{dsInstrStep = (pure . step)} -- | 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 :: Instr i o -> RemFail Instr i o analyzeInstrFailure = go where go :: Instr i o -> RemFail Instr i o go i' = i' & withClassifiedInstr \case SAlwaysFailing -> \case C_FAILWITH -> RfAlwaysFails FAILWITH C_NEVER -> RfAlwaysFails NEVER SFailingNormal -> withClassifiedInstr \case SNoChildren -> const $ RfNormal i' SHasIndirectChildren -> const $ RfNormal i' SMayHaveChildren -> \case C_Ext _ -> RfNormal i' STwoChildren -> \case C_Seq a b -> Seq a `rfMapAnyInstr` go b C_IF_NONE l r -> rfMerge IF_NONE (go l) (go r) C_IF_LEFT l r -> rfMerge IF_LEFT (go l) (go r) C_IF_CONS l r -> rfMerge IF_CONS (go l) (go r) C_IF l r -> rfMerge IF (go l) (go r) SOneChild -> withClassifiedInstr \case SFromMichelson -> const $ RfNormal i' _ -> \case C_WithLoc loc i -> WithLoc loc `rfMapAnyInstr` go i C_Meta meta i -> Meta meta `rfMapAnyInstr` go i C_Nested i -> Nested `rfMapAnyInstr` go i C_DocGroup g i -> DocGroup g `rfMapAnyInstr` go 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 -- v'Seq' have a single instruction (i. e. not v'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. DfsSettings Identity -> Value t -> Value t dfsMapValue settings v = runIdentity $ dfsTraverseValue settings v -- | Traverse a value in depth-first order. dfsTraverseValue :: forall t m. (Monad m) => DfsSettings m -> Value t -> m (Value t) dfsTraverseValue settings@DfsSettings{..} i = case i of -- Atomic VKey{} -> dsValueStep i VUnit -> dsValueStep i VSignature{} -> dsValueStep i VChainId{} -> dsValueStep i VOp{} -> dsValueStep i VContract{} -> dsValueStep i VTicket{} -> dsValueStep i -- cannot appear as constant in a contract VLam code -> case code of LambdaCode lambda -> do v <- fmap (VLam . LambdaCode . analyzeInstrFailure) $ dfsTraverseInstr settings (rfAnyInstr lambda) dsValueStep v LambdaCodeRec lambda -> do v <- fmap (VLam . LambdaCodeRec . analyzeInstrFailure) $ dfsTraverseInstr settings (rfAnyInstr lambda) dsValueStep v VInt{} -> dsValueStep i VNat{} -> dsValueStep i VString{} -> dsValueStep i VBytes{} -> dsValueStep i VMutez{} -> dsValueStep i VBool{} -> dsValueStep i VKeyHash{} -> dsValueStep i VBls12381Fr{} -> dsValueStep i VBls12381G1{} -> dsValueStep i VBls12381G2{} -> dsValueStep i VTimestamp{} -> dsValueStep i VAddress{} -> dsValueStep i VChestKey{} -> dsValueStep i VChest{} -> dsValueStep i -- Non-atomic VOption mVal -> case mVal of Nothing -> dsValueStep i Just val -> recursion1 (VOption . Just) val VList vals -> do vs <- traverse (dfsTraverseValue settings) vals dsValueStep $ VList vs VSet vals -> do cs <- S.fromList <$> traverse (dfsTraverseValue settings) (S.toList vals) dsValueStep (VSet cs) VPair (v1, v2) -> do v1' <- dfsTraverseValue settings v1 v2' <- dfsTraverseValue settings v2 dsValueStep $ 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 settings v dsValueStep $ 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 settings k v' <- dfsTraverseValue settings v pure (k', v') dsValueStep $ 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 (def{ dsValueStep = (\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 'Morley.Michelson.Typed.Haskell.Value.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 VChest{} -> ConstantStorage v VChestKey{} -> 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 (_ :: Set (Value t)) -> ConstantStorage v \\ comparableImplies (Proxy @t) 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))) | Dict <- comparableImplies (Proxy @tk) -> case checkScope @(ConstantScope tv) of Right Dict -> ConstantStorage v _ -> withDeMorganScope @ForbidOp @'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 -- | Whether this instruction is a real Michelson instruction. -- -- Only the root is in question, children in the instruction tree are not -- accounted for. -- -- >>> isMichelsonInstr (Seq Nop Nop) -- True -- -- >>> isMichelsonInstr (Ext $ COMMENT_ITEM "comment") -- False -- -- This function is helpful e.g. in debugger. isMichelsonInstr :: Instr i o -> Bool isMichelsonInstr = withClassifiedInstr $ const . \case SFromMichelson -> True SStructural -> True SAdditional -> False SPhantom -> False -- | A wrapper around either typechecked 'Anns' or unchecked 'NonEmpty' of -- 'AnyAnn'. Annotations on some instructions aren't typechecked, hence these -- two constructors. -- -- Helper for 'instrAnns'. data SomeAnns where SomeAnns :: Anns xs -> SomeAnns SomeUncheckedAnns :: NonEmpty AnyAnn -> SomeAnns -- | Get annotations from a typed 'Instr'. This doesn't recurse, use with -- 'dfsFoldInstr' to collect all annotations in a tree/sequence. instrAnns :: Instr i o -> Maybe SomeAnns instrAnns = withClassifiedInstr \case SDoesNotHaveAnns -> const mzero SDoesHaveStandardAnns -> $(do TH.TyConI (TH.DataD _ _ _ _ cons _) <- TH.reify ''ClassifiedInstr let mkMatch = \case TH.ForallC _ _ con -> mkMatch con TH.GadtC [nm] tys res | null $ listify (== 'DoesHaveStandardAnns) res -> Nothing | otherwise -> Just $ TH.match (TH.conP nm ([p|anns|] : replicate (length $ drop 1 tys) TH.wildP)) (TH.normalB [|pure $ SomeAnns anns|]) [] x -> error $ "Unexpected constructor " <> show (TH.ppr x) TH.lamCaseE $ mapMaybe mkMatch cons ) SDoesHaveNonStandardAnns -> \case C_AnnMIN_BLOCK_TIME anns -> SomeUncheckedAnns <$> nonEmpty anns C_AnnEMIT anns' tag ty -> pure $ case ty of Just ty' -> SomeAnns $ tag `AnnsCons` ty' `AnnsTyCons` anns' Nothing -> SomeAnns $ tag `AnnsCons` anns'