packages feed

morley-1.19.1: src/Morley/Michelson/Optimizer.hs

-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- NOTE this pragmas.
-- We disable some warnings for the sake of speed up.
-- Write code with care.
{-# OPTIONS_GHC -Wno-incomplete-patterns #-}
{-# OPTIONS_GHC -Wno-overlapping-patterns #-}

-- | Optimizer for typed instructions.
--
-- It's quite experimental and incomplete.

module Morley.Michelson.Optimizer
  ( optimize
  , optimizeWithConf
  , optimizeVerboseWithConf
  , defaultOptimizerConf
  , defaultRules
  , defaultRulesAndPushPack
  , orRule
  , orSimpleRule
  , Rule (..)
  , OptimizerConf (..)
  , ocGotoValuesL
    -- * Ruleset manipulation
  , OptimizationStage (..)
  , Ruleset
  , rulesAtPrio
  , insertRuleAtPrio
  , clearRulesAtPrio
  , alterRulesAtPrio
  , OptimizerStageStats(..)
  ) where

import Prelude hiding (EQ, GT, LT)

import Control.Lens (makeLensesFor)
import Data.Constraint (Dict(..), (\\))
import Data.Default (Default(def))
import Data.Map qualified as Map
import Data.Singletons (sing)
import Data.Type.Equality ((:~:)(Refl))
import Fmt (Buildable(..), (+|), (|+))

import Morley.Michelson.Interpret.Pack (packValue')
import Morley.Michelson.Optimizer.Internal.Proofs
import Morley.Michelson.Typed.Aliases (Value)
import Morley.Michelson.Typed.Arith
import Morley.Michelson.Typed.ClassifiedInstr
import Morley.Michelson.Typed.Instr hiding ((:#))
import Morley.Michelson.Typed.Scope (ConstantScope, PackedValScope, checkScope)
import Morley.Michelson.Typed.Sing
import Morley.Michelson.Typed.T
import Morley.Michelson.Typed.Util (DfsSettings(..), dfsFoldInstr, dfsTraverseInstr)
import Morley.Michelson.Typed.Value
import Morley.Util.Peano
import Morley.Util.PeanoNatural

pattern (:#) :: Instr inp b -> Instr b out -> Instr inp out
pattern l :# r <- (maybeSeq -> Seq l r)
  where l :# Nop = l
        Nop :# r = r
        l :# r = Seq l r
infixr 8 :#

maybeSeq :: Instr inp out -> Instr inp out
maybeSeq = \case
  x@Seq{} -> x
  x -> Seq x Nop

----------------------------------------------------------------------------
-- High level
----------------------------------------------------------------------------

-- | Optimization stages. Stages are run in first to last order, each stage has
-- an 'Int' argument, which allows splitting each stage into sub-stages, which
-- will run lowest index to highest. All default rules use sub-stage @0@.
data OptimizationStage
  = OptimizationStagePrepare Int
  | OptimizationStageMain Int
    -- ^ Main optimisation stage, except rules that would interfere with other
    -- rules.
  | OptimizationStageMainExtended Int
    -- ^ All main stage rules.
  | OptimizationStageRollAdjacent Int
    -- ^ Main stage rules unroll @DROP n@, @PAIR n@, etc into their primitive
    -- counterparts to simplify some optimisations. This stages coalesces them
    -- back.
  deriving stock (Eq, Ord)

instance Buildable OptimizationStage where
  build = \case
    OptimizationStagePrepare n -> "prepare " +| n |+ ""
    OptimizationStageMain n -> "main " +| n |+ ""
    OptimizationStageMainExtended n -> "main extended " +| n |+ ""
    OptimizationStageRollAdjacent n -> "roll adjacent " +| n |+ ""

-- | A set of optimization stages. Rules at the same sub-stage are applied in
-- arbitrary order. See 'OptimizationStage' for explanation of sub-stages.
--
-- 'Default' ruleset is empty.
newtype Ruleset = Ruleset { unRuleset :: Map OptimizationStage (NonEmpty Rule) }
  deriving newtype Default

instance Semigroup Ruleset where
  Ruleset l <> Ruleset r = Ruleset $ Map.unionWith (<>) l r

instance Monoid Ruleset where
  mempty = def

-- | Get rules for a given priority as a list.
rulesAtPrio :: OptimizationStage -> Ruleset -> [Rule]
rulesAtPrio prio = maybe [] toList . Map.lookup prio . unRuleset

-- | Insert a single rule at a given priority without touching other rules.
insertRuleAtPrio :: OptimizationStage -> Rule -> Ruleset -> Ruleset
insertRuleAtPrio = flip $ alterRulesAtPrio . (:)

-- | Remove the stage with the given priority.
clearRulesAtPrio :: OptimizationStage -> Ruleset -> Ruleset
clearRulesAtPrio = alterRulesAtPrio (const [])

-- | Alter all stage rules for a given priority.
alterRulesAtPrio :: ([Rule] -> [Rule]) -> OptimizationStage -> Ruleset -> Ruleset
alterRulesAtPrio f prio = Ruleset . Map.alter (nonEmpty . f . maybe [] toList) prio . unRuleset

data OptimizerConf = OptimizerConf
  { ocGotoValues :: Bool
  , ocRuleset    :: Ruleset
  , ocMaxIterations :: Word
  }

-- | Default config - all commonly useful rules will be applied to all the code.
defaultOptimizerConf :: OptimizerConf
defaultOptimizerConf = OptimizerConf
  { ocGotoValues = True
  , ocRuleset    = defaultRules
  , ocMaxIterations = 100
  }

instance Default OptimizerConf where
  def = defaultOptimizerConf

-- | Optimize a typed instruction by replacing some sequences of
-- instructions with smaller equivalent sequences.
-- Applies default set of rewrite rules.
optimize :: Instr inp out -> Instr inp out
optimize = optimizeWithConf def

-- | Optimize a typed instruction using a custom set of rules.
-- The set is divided into several stages, as applying
-- some rules can prevent others to be performed.
--
-- If any stage resulted in optimizations, we apply it again until we reach
-- fixpoint, but no more than 'ocMaxIterations' times.
optimizeWithConf :: OptimizerConf -> Instr inp out -> Instr inp out
optimizeWithConf = snd ... optimizeVerboseWithConf

data OptimizerStageStats = OptimizerStageStats
  { ossStage :: OptimizationStage
  , ossNumIterations :: Word
  , ossNumInstrs :: Word
  }

instance Buildable OptimizerStageStats where
  build OptimizerStageStats{..} =
    "Stage " +| ossStage
      |+ " finished after " +| ossNumIterations
      |+ " iterations. Instruction count: " +| ossNumInstrs |+ ""

-- | Returns some optimizer statistics in addition to optimized instruction.
-- Mostly useful for testing and debugging.
optimizeVerboseWithConf
  :: OptimizerConf
  -> Instr inp out
  -> ([OptimizerStageStats], Instr inp out)
optimizeVerboseWithConf OptimizerConf{..} instr =
  foldlM (performOneStage 1) instrRHS $ toPairs $ unRuleset ocRuleset
  where
    performOneStage n i stage@(stageName, stageRules)
      | not changed || n >= ocMaxIterations = ([OptimizerStageStats stageName n stats], res)
      | otherwise = performOneStage (succ n) res stage
      where
        stats = getSum $ instrCount res
        instrCount = dfsFoldInstr def { dsGoToValues = ocGotoValues } $ withClassifiedInstr \case
          SFromMichelson -> const 1
          _ -> const 0
        stageRule = fixpoint $ foldl orSimpleRule flattenSeqLHS stageRules
        (getAny -> changed, res) =
          dfsTraverseInstr def{ dsGoToValues = ocGotoValues, dsInstrStep = applyOnce stageRule } i
    instrRHS = snd $ applyOnce (fixpoint flattenSeqLHS) instr

----------------------------------------------------------------------------
-- Rewrite rules
----------------------------------------------------------------------------

{-
Note [Writing optimizer rules]
------------------------------

We locally redefine (:#) to simplify handling instruction sequence tails.
Consider that a given instruction sequence can appear in the middle, and then @a
:# b :# tail@ will match, or at the end of the sequence, and then @a :# b@ will
match.

Local definition of (:#) makes it so we can always assume there's a tail.
However, we don't need it when matching on single instructions.

Thus, the rule of thumb is this: if you're matching on a single instruction,
everything is fine. If you're matching on a sequence, i.e. using (:#), then
always match on tail.
-}

-- Type of a single rewrite rule, wrapped in `newtype`. It takes an instruction
-- and tries to optimize its head (first few instructions). If optimization
-- succeeds, it returns `Just` the optimized instruction, otherwise it returns `Nothing`.
newtype Rule = Rule {unRule :: forall inp out. Instr inp out -> Maybe (Instr inp out)}

-- | Default optimization rules.
defaultRules :: Ruleset
defaultRules = foldr ($) def $ uncurry (alterRulesAtPrio . const) <$>
  -- NB: if adding more main stages, remember to check if
  -- 'defaultRulesAndPushPack' needs updating.
  [ (mainStageRules              , OptimizationStageMain 0)
  , (dipDrop2swapDropStageRules  , OptimizationStageMainExtended 0)
  , (glueAdjacentInstrsStageRules, OptimizationStageRollAdjacent 0)
  ]
  where
    dipDrop2swapDropStageRules = dipDrop2swapDrop : mainStageRules
    glueAdjacentInstrsStageRules =
      [ adjacentDrops
      , rollPairN
      , rollUnpairN
      , rollDips
      ]

-- | We do not enable 'pushPack' rule by default because it is potentially
-- dangerous. There are various code processing functions that may depend on
-- constants, e. g. string transformations.
defaultRulesAndPushPack :: Ruleset
defaultRulesAndPushPack = defaultRules
  & insertRuleAtPrio (OptimizationStageMainExtended 0) pushPack

mainStageRules :: [Rule]
mainStageRules =
  [ removeNesting
  , removeExtStackType
  , ifNopNop2Drop
  , nopIsNeutralForSeq
  , variousNops
  , dupSwap2dup
  , noDipNeeded
  , branchShortCut
  , compareWithZero
  , internalNop
  , simpleDups
  , adjacentDips
  , isSomeOnIf
  , redundantIf
  , emptyDip
  , digDug
  , specificPush
  , pairUnpair
  , pairMisc
  , unpairMisc
  , swapBeforeCommutative
  , justDrops
  , justDoubleDrops
  , dig1AndDug1AreSwap
  , notIf
  , dropMeta
  -- strictly speaking, these rules below are de-optimisations, but they
  -- expose opportunities for other optimisations, and they are undone in the
  -- last stage.
  , unrollPairN
  , unrollUnpairN
  , unrollDropN
  , unrollDips
  ]

flattenSeqLHS :: Rule -> Rule
flattenSeqLHS toplevel = Rule $ \case
  it@(Seq (Seq _ _) _) -> Just $ linearizeAndReapply toplevel it
  _                    -> Nothing

dropMeta :: Rule
dropMeta = Rule $ \case
  Meta _ i -> Just i
  WithLoc _ i -> Just i
  _        -> Nothing

removeNesting :: Rule
removeNesting = Rule $ \case
  Nested i -> Just i
  _        -> Nothing

-- | STACKTYPE is currently a Nop and may safely be removed.
removeExtStackType :: Rule
removeExtStackType = Rule $ \case
  Ext (STACKTYPE{}) -> Just Nop
  _                 -> Nothing

dipDrop2swapDrop :: Rule
dipDrop2swapDrop = Rule $ \case
  DIP DROP -> Just $ SWAP :# DROP
  _        -> Nothing

ifNopNop2Drop :: Rule
ifNopNop2Drop = Rule $ \case
  IF Nop Nop -> Just DROP
  _          -> Nothing

nopIsNeutralForSeq :: Rule
nopIsNeutralForSeq = Rule $ \case
  -- NB: we're not using (:#) here because it'll always match when rhs is Nop
  Seq Nop i -> Just i
  Seq i Nop -> Just i
  _         -> Nothing

variousNops :: Rule
variousNops = Rule $ \case
  DUP                :# DROP :# c -> Just c
  DUPN _             :# DROP :# c -> Just c
  SWAP               :# SWAP :# c -> Just c
  PUSH _             :# DROP :# c -> Just c
  NONE               :# DROP :# c -> Just c
  UNIT               :# DROP :# c -> Just c
  NIL                :# DROP :# c -> Just c
  EMPTY_SET          :# DROP :# c -> Just c
  EMPTY_MAP          :# DROP :# c -> Just c
  EMPTY_BIG_MAP      :# DROP :# c -> Just c
  LAMBDA _           :# DROP :# c -> Just c
  SELF _             :# DROP :# c -> Just c
  NOW                :# DROP :# c -> Just c
  AMOUNT             :# DROP :# c -> Just c
  BALANCE            :# DROP :# c -> Just c
  TOTAL_VOTING_POWER :# DROP :# c -> Just c
  SOURCE             :# DROP :# c -> Just c
  SENDER             :# DROP :# c -> Just c
  CHAIN_ID           :# DROP :# c -> Just c
  LEVEL              :# DROP :# c -> Just c
  SELF_ADDRESS       :# DROP :# c -> Just c
  READ_TICKET        :# DROP :# c -> Just c
  _                          -> Nothing

dupSwap2dup :: Rule
dupSwap2dup = Rule $ \case
  DUP :# SWAP :# c -> Just $ DUP :# c
  _                -> Nothing

noDipNeeded :: Rule
noDipNeeded = Rule $ \case
  -- If we put a constant value on stack and then do something under it,
  -- we can do this "something" on original stack and then put that constant.
  PUSH x    :# DIP f :# c -> Just $ f :# PUSH x :# c
  UNIT      :# DIP f :# c -> Just $ f :# UNIT :# c
  NOW       :# DIP f :# c -> Just $ f :# NOW :# c
  SENDER    :# DIP f :# c -> Just $ f :# SENDER :# c
  EMPTY_MAP :# DIP f :# c -> Just $ f :# EMPTY_MAP :# c
  EMPTY_SET :# DIP f :# c -> Just $ f :# EMPTY_SET :# c

  -- If we do something ignoring top of the stack and then immediately
  -- drop top of the stack, we can drop that item in advance and
  -- not use 'DIP' at all.
  DIP f :# DROP :# c -> Just $ DROP :# f :# c

  _ -> Nothing

unrollDips :: Rule
unrollDips = Rule go
  where
    go :: Instr inp out -> Maybe (Instr inp out)
    go = \case
      DIPN Zero c -> Just $ c
      DIPN One c -> Just $ DIP c
      DIPN (Succ n) c -> DIP <$> go (DIPN n c)
      _ -> Nothing

rollDips :: Rule
rollDips = Rule $ \case
  DIP (DIP c) -> Just $ DIPN Two c
  DIP (DIPN n c) -> Just $ DIPN (Succ n) c
  _ -> Nothing

branchShortCut :: Rule
branchShortCut = Rule $ \case
  LEFT  :# IF_LEFT f _ :# c -> Just $ f :# c
  RIGHT :# IF_LEFT _ f :# c -> Just $ f :# c
  CONS  :# IF_CONS f _ :# c -> Just $ f :# c
  NIL   :# IF_CONS _ f :# c -> Just $ f :# c
  NONE  :# IF_NONE f _ :# c -> Just $ f :# c
  SOME  :# IF_NONE _ f :# c -> Just $ f :# c

  PUSH vOr@(VOr eitherVal) :# IF_LEFT f g :# c -> case vOr of
    (_ :: Value ('TOr l r)) -> case eitherVal of
      Left val -> case checkScope @(ConstantScope l) of
        Right Dict -> Just $ PUSH val :# f :# c
        _          -> Nothing
      Right val -> case checkScope @(ConstantScope r) of
        Right Dict -> Just $ PUSH val :# g :# c
        _          -> Nothing
  PUSH (VList (x : xs))     :# IF_CONS f _ :# c -> Just $ PUSH (VList xs) :# PUSH x :# f :# c
  PUSH (VList _)            :# IF_CONS _ f :# c -> Just $ f :# c
  PUSH (VOption Nothing)    :# IF_NONE f _ :# c -> Just $ f :# c
  PUSH (VOption (Just val)) :# IF_NONE _ f :# c -> Just $ PUSH val :# f :# c
  PUSH (VBool True)         :# IF f _      :# c -> Just $ f :# c
  PUSH (VBool False)        :# IF _ f      :# c -> Just $ f :# c
  _ -> Nothing

compareWithZero :: Rule
compareWithZero = Rule $ \case
  PUSH (VInt 0) :# COMPARE :# EQ :# c -> Just $ EQ :# c
  PUSH (VNat 0) :# COMPARE :# EQ :# c -> Just $ INT :# EQ :# c
  _                                   -> Nothing

-- If an instruction takes another instruction as an argument and that
-- internal instruction is 'Nop', sometimes the whole instruction is
-- 'Nop'.
-- For now we do it only for 'DIP', but ideally we should do it for
-- 'MAP' as well (which is harder).
internalNop :: Rule
internalNop = Rule $ \case
  DIP Nop      -> Just Nop

  _ -> Nothing

simpleDups :: Rule
simpleDups = Rule $ \case
  -- DUP 1 is just DUP
  DUPN One -> Just DUP

  _ -> Nothing

adjacentDips :: Rule
adjacentDips = Rule $ \case
  DIP f :# DIP g :# c -> Just $ DIP (f :# g) :# c

  _ -> Nothing

redundantIf :: Rule
redundantIf = Rule \case
  IF x y
    | x == y
    -> Just $ DROP :# x
  _ -> Nothing

emptyDip :: Rule
emptyDip = Rule \case
  DIP Nop -> Just Nop
  DIPN _ Nop -> Just Nop
  _ -> Nothing

digDug :: Rule
digDug = Rule \case
  DIG x :# DUG y :# c | Just Refl <- eqPeanoNat x y -> Just c
  _ -> Nothing

-- | Sequences of @DROP@s can be turned into single @DROP n@.
-- When @n@ is greater than 2 it saves size and gas.
-- When @n@ is 2 it saves gas only.
adjacentDrops :: Rule
adjacentDrops = Rule $ \case
  DROP :# DROP :# c -> Just $ DROPN Two :# c

  (DROPN n :: Instr inp out) :# (DROP :: Instr inp' out') :# c
     -> Just $ DROPN (Succ n) :# c \\ dropNDropNProof @inp @out @out' n One
                                   \\ commutativity (singPeanoNat n) (singPeanoNat One)

  (DROP :: Instr inp out) :# (DROPN n :: Instr inp' out') :# c
     -> Just $ DROPN (Succ n) :# c \\ dropNDropNProof @inp @out @out' One n

  (DROPN n :: Instr inp out) :# (DROPN m :: Instr inp' out') :# c
     -> Just $ DROPN (addPeanoNat n m) :# c \\ dropNDropNProof @inp @out @out' n m

  _ -> Nothing

specificPush :: Rule
specificPush = Rule $ \case
  push@PUSH{} :# c -> (:# c) <$> optimizePush push

  _ -> Nothing
  where
    optimizePush :: Instr inp out -> Maybe (Instr inp out)
    optimizePush = \case
      PUSH v | _ :: Value v <- v -> case v of
        VUnit -> Just UNIT
        VMap m
          | null m -> case sing @v of STMap{} -> Just EMPTY_MAP
        VSet m
          | null m -> case sing @v of STSet{} -> Just EMPTY_SET
        _ -> Nothing

      _ -> Nothing


isSomeOnIf :: Rule
isSomeOnIf = Rule $ \case
  IF (PUSH (VOption Just{})) (PUSH (VOption Nothing))
    :# IF_NONE (PUSH (VBool False)) (DROP :# PUSH (VBool True))
    :# c
    -> Just c
  _ -> Nothing

pairUnpair :: Rule
pairUnpair = Rule $ \case
  PAIR :# UNPAIR :# c -> Just c

  UNPAIR :# PAIR :# c -> Just c

  _ -> Nothing

pairMisc :: Rule
pairMisc = Rule $ \case
  PAIR :# CDR :# c -> Just $ DROP :# c

  PAIR :# CAR :# c -> Just $ (DIP DROP) :# c

  _ -> Nothing

unpairMisc :: Rule
unpairMisc = Rule $ \case
  DUP :# CAR :# DIP CDR :# c -> Just $ UNPAIR :# c

  DUP :# CDR :# DIP CAR :# c -> Just $ UNPAIR :# SWAP :# c

  UNPAIR :# DROP :# c        -> Just $ CDR :# c
  _ -> Nothing

unrollDropN :: Rule
unrollDropN = Rule go
  where
    go :: forall inp out. Instr inp out -> Maybe (Instr inp out)
    go = \case
      DROPN Zero -> Just Nop
      DROPN (Succ n) -> do
        dropn <- go $ DROPN n
        Just $ DROP :# dropn
        \\ unconsListProof @inp n
      _ -> Nothing

unrollPairN :: Rule
unrollPairN = Rule go
  where
    go :: forall inp out. Instr inp out -> Maybe (Instr inp out)
    go = \case
      PAIRN Two -> Just PAIR \\ pairN2isPairProof @inp
      PAIRN (Succ n@(Succ Succ{}) ) -> do
        pairn <- go $ PAIRN n
        Just $ DIP pairn :# PAIR
        \\ unconsListProof @inp n
      _ -> Nothing

unrollUnpairN :: Rule
unrollUnpairN = Rule go
  where
    go :: forall inp out. Instr inp out -> Maybe (Instr inp out)
    go = \case
      UNPAIRN Two -> Just UNPAIR \\ unpairN2isUnpairProof @inp @out
      UNPAIRN (Succ n@(Succ Succ{})) -> do
        unpairn <- go $ UNPAIRN n
        Just $ UNPAIR :# DIP unpairn
        \\ unpairNisUnpairDipUnpairNProof @inp @out n
      _ -> Nothing

rollPairN :: Rule
rollPairN = Rule $ \case
  DIP PAIR :# PAIR :# c -> Just $ PAIRN (Succ Two) :# c

  (DIP (PAIRN n@(Succ Succ{})) :: Instr inp out) :# PAIR :# c -> Just $ PAIRN (Succ n) :# c
    \\ dipPairNPairIsPairNProof @inp n

  _ -> Nothing

rollUnpairN :: Rule
rollUnpairN = Rule $ \case
  UNPAIR :# DIP UNPAIR :# c -> Just $ UNPAIRN (Succ Two) :# c
  UNPAIR :# DIP (UNPAIRN n@(Succ Succ{})) :# c -> Just $ UNPAIRN (Succ n) :# c
  _ -> Nothing

commuteArith ::
  forall n m s out. Instr (n ': m ': s) out -> Maybe (Instr (m ': n ': s) out)
commuteArith = \case
  ADD -> do Dict <- commutativityProof @Add @n @m; Just ADD
  MUL -> do Dict <- commutativityProof @Mul @n @m; Just MUL
  OR -> do Dict <- commutativityProof @Or @n @m; Just OR
  AND -> do Dict <- commutativityProof @And @n @m; Just AND
  XOR -> do Dict <- commutativityProof @Xor @n @m; Just XOR
  _ -> Nothing

swapBeforeCommutative :: Rule
swapBeforeCommutative = Rule $ \case
  SWAP :# i :# c -> (:# c) <$> commuteArith i

  _ -> Nothing

pushPack :: Rule
pushPack = Rule $ \case
  PUSH x :# PACK :# c -> Just $ pushPacked x :# c

  _ -> Nothing
  where
    pushPacked :: PackedValScope t => Value t -> Instr s ('TBytes ': s)
    pushPacked = PUSH . VBytes . packValue'

justDrops :: Rule
justDrops = Rule $ \case
  CAR              :# DROP :# c -> Just $ DROP :# c
  CDR              :# DROP :# c -> Just $ DROP :# c
  SOME             :# DROP :# c -> Just $ DROP :# c
  LEFT             :# DROP :# c -> Just $ DROP :# c
  RIGHT            :# DROP :# c -> Just $ DROP :# c
  SIZE             :# DROP :# c -> Just $ DROP :# c
  GETN _           :# DROP :# c -> Just $ DROP :# c
  CAST             :# DROP :# c -> Just $ DROP :# c
  RENAME           :# DROP :# c -> Just $ DROP :# c
  PACK             :# DROP :# c -> Just $ DROP :# c
  UNPACK           :# DROP :# c -> Just $ DROP :# c
  CONCAT'          :# DROP :# c -> Just $ DROP :# c
  ISNAT            :# DROP :# c -> Just $ DROP :# c
  ABS              :# DROP :# c -> Just $ DROP :# c
  NEG              :# DROP :# c -> Just $ DROP :# c
  NOT              :# DROP :# c -> Just $ DROP :# c
  EQ               :# DROP :# c -> Just $ DROP :# c
  NEQ              :# DROP :# c -> Just $ DROP :# c
  LT               :# DROP :# c -> Just $ DROP :# c
  GT               :# DROP :# c -> Just $ DROP :# c
  LE               :# DROP :# c -> Just $ DROP :# c
  GE               :# DROP :# c -> Just $ DROP :# c
  INT              :# DROP :# c -> Just $ DROP :# c
  CONTRACT _       :# DROP :# c -> Just $ DROP :# c
  SET_DELEGATE     :# DROP :# c -> Just $ DROP :# c
  IMPLICIT_ACCOUNT :# DROP :# c -> Just $ DROP :# c
  VOTING_POWER     :# DROP :# c -> Just $ DROP :# c
  SHA256           :# DROP :# c -> Just $ DROP :# c
  SHA512           :# DROP :# c -> Just $ DROP :# c
  BLAKE2B          :# DROP :# c -> Just $ DROP :# c
  SHA3             :# DROP :# c -> Just $ DROP :# c
  KECCAK           :# DROP :# c -> Just $ DROP :# c
  HASH_KEY         :# DROP :# c -> Just $ DROP :# c
  PAIRING_CHECK    :# DROP :# c -> Just $ DROP :# c
  ADDRESS          :# DROP :# c -> Just $ DROP :# c
  JOIN_TICKETS     :# DROP :# c -> Just $ DROP :# c
  _                             -> Nothing

justDoubleDrops :: Rule
justDoubleDrops = Rule $ \case
  PAIR         :# DROP :# c -> Just $ DROP :# DROP :# c
  MEM          :# DROP :# c -> Just $ DROP :# DROP :# c
  GET          :# DROP :# c -> Just $ DROP :# DROP :# c
  APPLY        :# DROP :# c -> Just $ DROP :# DROP :# c
  CONCAT       :# DROP :# c -> Just $ DROP :# DROP :# c
  ADD          :# DROP :# c -> Just $ DROP :# DROP :# c
  SUB          :# DROP :# c -> Just $ DROP :# DROP :# c
  SUB_MUTEZ    :# DROP :# c -> Just $ DROP :# DROP :# c
  MUL          :# DROP :# c -> Just $ DROP :# DROP :# c
  EDIV         :# DROP :# c -> Just $ DROP :# DROP :# c
  OR           :# DROP :# c -> Just $ DROP :# DROP :# c
  AND          :# DROP :# c -> Just $ DROP :# DROP :# c
  XOR          :# DROP :# c -> Just $ DROP :# DROP :# c
  COMPARE      :# DROP :# c -> Just $ DROP :# DROP :# c
  TICKET       :# DROP :# c -> Just $ DROP :# DROP :# c
  SPLIT_TICKET :# DROP :# c -> Just $ DROP :# DROP :# c
  SWAP :# DROP :# DROP :# c -> Just $ DROP :# DROP :# c
  _ -> Nothing

dig1AndDug1AreSwap :: Rule
dig1AndDug1AreSwap = Rule \case
  DUG One -> Just SWAP
  DIG One -> Just SWAP
  _ -> Nothing

notIf :: Rule
notIf = Rule \case
  (NOT :: Instr inp out) :# IF a b :# c | STBool <- sing @(Head inp) -> Just $ IF b a :# c
  _ -> Nothing

-- | Append LHS of v'Seq' to RHS and re-run pointwise ocRuleset at each point.
--   That might cause reinvocation of this function (see 'defaultRules'),
--   but effectively this ensures it will flatten any v'Seq'-tree right-to-left,
--   while evaling no more than once on each node.
--
--   The reason this function invokes ocRuleset is when you append an instr
--   to already-optimised RHS of v'Seq', you might get an optimisable tree.
--
--   The argument is a local, non-structurally-recursive ocRuleset.
linearizeAndReapply :: Rule -> Instr inp out -> Instr inp out
linearizeAndReapply restart = snd . \case
  Seq (Seq a b) c ->
    applyOnce restart $ Seq a (linearizeAndReapply restart (Seq b c))

  other -> applyOnce restart other

----------------------------------------------------------------------------
-- Generic functions working with rules
----------------------------------------------------------------------------

-- | Combine two rule fixpoints.
orRule :: (Rule -> Rule) -> (Rule -> Rule) -> (Rule -> Rule)
orRule l r topl = Rule $ \instr ->
  (unRule (l topl) $ instr) <|> (unRule (r topl) $ instr)

-- | Combine a rule fixpoint and a simple rule.
orSimpleRule :: (Rule -> Rule) -> Rule -> (Rule -> Rule)
orSimpleRule l r topl = Rule $ \instr ->
  (unRule (l topl) $ instr) <|> (unRule r $ instr)

-- | Turn rule fixpoint into rule.
fixpoint :: (Rule -> Rule) -> Rule
fixpoint r = go
  where
    go :: Rule
    go = whileApplies (r go)

-- | Apply the rule once, if it fails, return the instruction unmodified.
--
-- Also returns a flag showing whether the rule succeeded or not.
applyOnce :: Rule -> Instr inp out -> (Any Bool, Instr inp out)
applyOnce r i = maybe (pure i) (Any True,) (unRule r $ i)

-- | Apply a rule to the same code, until it fails.
whileApplies :: Rule -> Rule
whileApplies r = Rule $ go <=< unRule r
  -- NB: if the rule doesn't apply even once, we want to return Nothing here,
  -- hence it's first applied above and only if successful goes into recursion.
  where
    go :: Instr inp out -> Maybe (Instr inp out)
    go i = maybe (Just i) go (unRule r i)

----------------------------------------------------------------------------
-- TH
----------------------------------------------------------------------------

makeLensesFor [("ocGotoValues", "ocGotoValuesL")] ''OptimizerConf