packages feed

morley-1.19.2: src/Morley/Michelson/Optimizer/Utils.hs

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

-- | Utilities for writing optimizer rules.
module Morley.Michelson.Optimizer.Utils
  ( pattern (:#)
  , orRule
  , orSimpleRule
  , fixpoint
  , applyOnce
  , whileApplies
  , linearizeAndReapply
  ) where

import Morley.Michelson.Optimizer.Internal.Ruleset (Rule(..))
import Morley.Michelson.Typed.Instr hiding ((:#))

{- | This is a redefinition of @(:#)@ from "Morley.Michelson.Typed.Instr" that
is particularly useful for writing optimizer rules. When matching on an
instruction @x@ that isn't v'Seq', it behaves as if it matched on @x :# Nop@.

When constructing instructions using this pattern, @Nop@ is automatically
removed.

To understand why this is useful, consider that a given instruction sequence can
appear in the middle of a sequence, and then @a :# b :# tail@ will match, or at
the end of the sequence, and then @a :# b@ will match. Thus, to cover all cases
one would have to duplicate most rules.

This 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, e.g.

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

But this works, too:

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

-}
pattern (:#) :: Instr inp b -> Instr b out -> Instr inp out
pattern l :# r <- (\case { x@Seq{} -> x; x -> Seq x Nop } -> Seq l r)
  where l :# Nop = l
        Nop :# r = r
        l :# r = Seq l r
infixr 8 :#

-- | 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)

-- | Append LHS of v'Seq' to RHS and re-run pointwise ocRuleset at each point.
--   That might cause reinvocation of this function (see @defaultRule@'),
--   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