{-# LANGUAGE PatternSynonyms #-} -- For Comb
{-# LANGUAGE TemplateHaskell #-} -- For branch
{-# LANGUAGE ViewPatterns #-} -- For unSimplComb
{-# OPTIONS_GHC -fno-warn-orphans #-} -- For MakeLetName TH.Name
-- | Bottom-up optimization of 'Comb'inators,
-- reexamining downward as needed after each optimization.
module Symantic.Parser.Grammar.Optimize where

import Data.Bool (Bool(..), (&&), not)
import Data.Bifunctor (second)
import Data.Either (Either(..), either)
import Data.Eq (Eq(..))
import Data.Function (($), (.))
import Data.Kind (Constraint)
import Data.Maybe (Maybe(..))
import Data.Set (Set)
import Data.Functor.Identity (Identity(..))
import Unsafe.Coerce (unsafeCoerce)
import Type.Reflection (Typeable, typeRep, eqTypeRep, (:~~:)(..))
import Data.Semigroup (Semigroup(..))
import qualified Data.Foldable as Foldable
import qualified Data.Functor as F
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import Data.Hashable (Hashable)
import qualified Language.Haskell.TH as TH

import Symantic.Parser.Grammar.Combinators
import Symantic.Parser.Grammar.Production
import Symantic.Parser.Grammar.SharingObserver
import Symantic.Syntaxes.Derive
import qualified Symantic.Syntaxes.Classes as Prod
import qualified Symantic.Semantics.Data as Prod

{-
import Data.Function (($), flip)
import Debug.Trace (trace)

(&) = flip ($)
infix 0 &
-}
type OptimizeGrammar = KnotComb TH.Name

-- | TODO: remove useless wrapping?
newtype TiedComb repr a = TiedComb
  {  combSimpl :: SimplComb repr a
  --,  combRefs :: HS.HashSet letName
  }

-- * Type 'KnotComb'
data KnotComb letName repr a = KnotComb
  { knotCombOpens :: OpenRecs letName (SomeLet (TiedComb repr))
    -- ^ 'TiedComb' for all 'letName' in 'lets'.
  , knotCombOpen ::
      LetRecs letName (SomeLet (TiedComb repr)) ->
      TiedComb repr a
    -- ^ 'TiedComb' of the current combinator,
    -- with access to the final 'knotCombOpens'.
  }

optimizeGrammar ::
  Derivable (SimplComb repr) =>
  KnotComb TH.Name repr a -> repr a
optimizeGrammar = derive . derive

type instance Derived (KnotComb letName repr) = SimplComb repr
instance Derivable (KnotComb letName repr) where
  derive opt = combSimpl $
    knotCombOpen opt (mutualFix (knotCombOpens opt))
instance LiftDerived (KnotComb letName repr) where
  liftDerived x = KnotComb
    { knotCombOpens = HM.empty
    , knotCombOpen = \_final -> TiedComb
        { combSimpl = x
        }
    }
instance LiftDerived1 (KnotComb letName repr) where
  liftDerived1 f a = a
    { knotCombOpen = \final -> TiedComb
      { combSimpl = f (combSimpl (knotCombOpen a final))
      }
    }
instance (Eq letName, Hashable letName) => LiftDerived2 (KnotComb letName repr) where
  liftDerived2 f a b = KnotComb
    { knotCombOpens = knotCombOpens a <> knotCombOpens b
    , knotCombOpen = \final -> TiedComb
      { combSimpl = f
         (combSimpl (knotCombOpen a final))
         (combSimpl (knotCombOpen b final))
      }
    }
instance (Eq letName, Hashable letName) => LiftDerived3 (KnotComb letName repr) where
  liftDerived3 f a b c = KnotComb
    { knotCombOpens = HM.unions
      [ knotCombOpens a
      , knotCombOpens b
      , knotCombOpens c
      ]
    , knotCombOpen = \final -> TiedComb
      { combSimpl = f
         (combSimpl (knotCombOpen a final))
         (combSimpl (knotCombOpen b final))
         (combSimpl (knotCombOpen c final))
      }
    }
instance (Eq letName, Hashable letName) => LiftDerived4 (KnotComb letName repr) where
  liftDerived4 f a b c d = KnotComb
    { knotCombOpens = HM.unions
      [ knotCombOpens a
      , knotCombOpens b
      , knotCombOpens c
      , knotCombOpens d
      ]
    , knotCombOpen = \final -> TiedComb
      { combSimpl = f
         (combSimpl (knotCombOpen a final))
         (combSimpl (knotCombOpen b final))
         (combSimpl (knotCombOpen c final))
         (combSimpl (knotCombOpen d final))
      }
    }

-- * Data family 'Comb'
-- | 'Comb'inators of the 'Grammar'.
-- This is an extensible data-type.
data family Comb
  (comb :: ReprComb -> Constraint)
  :: ReprComb -> ReprComb
type instance Derived (Comb comb repr) = repr

-- | 'unsafeCoerce' restrained to 'SimplComb'.
-- Useful to avoid dependant-map when inlining.
unsafeSimplComb :: SimplComb repr a -> SimplComb repr b
unsafeSimplComb = unsafeCoerce

-- | Convenient utility to pattern-match a 'SimplComb'.
pattern Comb :: Typeable comb => Comb comb repr a -> SimplComb repr a
pattern Comb x <- (unSimplComb -> Just x)

-- ** Type 'SimplComb'
-- | Interpreter simplifying combinators.
-- Useful to handle a list of 'Comb'inators
-- without requiring impredicative quantification.
-- Must be used by pattern-matching
-- on the 'SimplComb' data-constructor,
-- to bring the constraints in scope.
--
-- The optimizations are directly applied within it,
-- to avoid introducing an extra newtype,
-- this also give a more understandable code.
data SimplComb repr a =
  forall comb.
  (Derivable (Comb comb repr), Typeable comb) =>
  SimplComb
    { combData :: Comb comb repr a
      -- ^ Some 'Comb'inator existentialized
      -- over the actual combinator symantic class.
    , combInline :: Bool
      -- ^ Whether this combinator must be inlined
      -- in place of a 'ref'erence pointing to it
      -- (instead of generating a 'call').
    , combRefs :: HS.HashSet TH.Name
      -- ^ 'ref''s names reacheable from combinator
      -- (including those behind 'ref's).
    }

type instance Derived (SimplComb repr) = repr
instance Derivable (SimplComb repr) where
  derive SimplComb{..} = derive combData

-- | @(unSimplComb c :: 'Maybe' ('Comb' comb repr a))@
-- extract the data-constructor from the given 'SimplComb'
-- iif. it belongs to the @('Comb' comb repr a)@ data-instance.
unSimplComb ::
  forall comb repr a.
  Typeable comb =>
  SimplComb repr a -> Maybe (Comb comb repr a)
unSimplComb SimplComb{ combData = c :: Comb c repr a } =
  case typeRep @comb `eqTypeRep` typeRep @c of
    Just HRefl -> Just c
    Nothing -> Nothing

-- CombAlternable
data instance Comb CombAlternable repr a where
  Alt :: Exception -> SimplComb repr a -> SimplComb repr a -> Comb CombAlternable repr a
  Empty :: Comb CombAlternable repr a
  Failure :: SomeFailure -> Comb CombAlternable repr a
  Throw :: ExceptionLabel -> Comb CombAlternable repr a
  Try :: SimplComb repr a -> Comb CombAlternable repr a
instance CombAlternable repr => Derivable (Comb CombAlternable repr) where
  derive = \case
    Alt exn x y -> alt exn (derive x) (derive y)
    Empty -> empty
    Failure sf -> failure sf
    Throw exn -> throw exn
    Try x -> try (derive x)
instance
  ( CombAlternable repr
  , CombApplicable repr
  , CombLookable repr
  , CombMatchable repr
  , CombSelectable repr
  ) => CombAlternable (SimplComb repr) where
  empty = SimplComb
    { combData = Empty
    , combInline = True
    , combRefs = HS.empty
    }
  failure sf = SimplComb
    { combData = Failure sf
    , combInline = True
    , combRefs = HS.empty
    }

  alt _exn p@(Comb Pure{}) _ = p
    -- & trace "Left Catch Law"
  alt _exn (Comb Empty) u = u
    -- & trace "Left Neutral Law"
  alt _exn u (Comb Empty) = u
    -- & trace "Right Neutral Law"
  alt exn (Comb (Alt exn' u v)) w | exn' == exn = u <|> (v <|> w)
    -- See Lemma 1 (Associativity of choice for labeled PEGs)
    -- in https://doi.org/10.1145/2851613.2851750
    -- & trace "Associativity Law"
  alt exn (Comb (Look p)) (Comb (Look q)) = look (alt exn (try p) q)
    -- & trace "Distributivity Law"
  alt exn x y = SimplComb
    { combData = Alt exn x y
    , combInline = False
    , combRefs = combRefs x <> combRefs y
    }

  throw exn = SimplComb
    { combData = Throw exn
    , combInline = True
    , combRefs = HS.empty
    }

  try (Comb (p :$>: x)) = try p $> x
    -- & trace "Try Interchange Law"
  try (Comb (f :<$>: p)) = f <$> try p
    -- & trace "Try Interchange Law"
  try x = SimplComb
    { combData = Try x
    , combInline = False
    , combRefs = combRefs x
    }
instance
  ( CombApplicable repr
  , CombAlternable repr
  , CombLookable repr
  , CombMatchable repr
  , CombSelectable repr
  , Eq letName
  , Hashable letName
  ) => CombAlternable (KnotComb letName repr)

-- CombApplicable
data instance Comb CombApplicable repr a where
  Pure :: Production '[] a -> Comb CombApplicable repr a
  (:<*>:) :: SimplComb repr (a -> b) -> SimplComb repr a -> Comb CombApplicable repr b
  (:<*:) :: SimplComb repr a -> SimplComb repr b -> Comb CombApplicable repr a
  (:*>:) :: SimplComb repr a -> SimplComb repr b -> Comb CombApplicable repr b
infixl 4 :<*>:, :<*:, :*>:
pattern (:<$>:) :: Production '[] (a -> b) -> SimplComb repr a -> Comb CombApplicable repr b
pattern t :<$>: x <- Comb (Pure t) :<*>: x
pattern (:$>:) :: SimplComb repr a -> Production '[] b -> Comb CombApplicable repr b
pattern x :$>: t <- x :*>: Comb (Pure t)
instance CombApplicable repr => Derivable (Comb CombApplicable repr) where
  derive = \case
    Pure x -> pure x
    f :<*>: x -> derive f <*> derive x
    x :<*: y -> derive x <* derive y
    x :*>: y -> derive x *> derive y
instance
  ( CombApplicable repr
  , CombAlternable repr
  , CombLookable repr
  , CombMatchable repr
  , CombSelectable repr
  ) => CombApplicable (SimplComb repr) where
  pure a = SimplComb
    { combData = Pure a
    , combInline = False -- TODO: maybe True?
    , combRefs = HS.empty
    }
  f <$> Comb (Branch b l r) =
    branch b
      ((Prod..) Prod..@ f <$> l)
      ((Prod..) Prod..@ f <$> r)
    -- & trace "Branch Distributivity Law"
  f <$> Comb (Conditional a bs def) =
    conditional a
      (second (f <$>) F.<$> bs)
      (f <$> def)
    -- & trace "Conditional Distributivity Law"
  -- Being careful here to use (<*>),
  -- instead of SimplComb { combData = f <$> combData x },
  -- in order to apply the optimizations of (<*>).
  f <$> x = pure f <*> x

  x <$ u = u $> x
    -- & trace "Commutativity Law"

  Comb Empty <*> _ = empty
    -- & trace "App Right Absorption Law"
  u <*> Comb Empty = u *> empty
    -- & trace "App Failure Weakening Law"
  Comb (Pure f) <*> Comb (Pure x) = pure (f Prod..@ x)
    -- & trace "Homomorphism Law"
  {-
  Comb (Pure f) <*> Comb (g :<$>: p) =
    -- This is basically a shortcut,
    -- it can be caught by one Composition Law
    -- and two Homomorphism Law.
    (Prod..) Prod..@ f Prod..@ g <$> p
    -- & trace "Functor Composition Law"
  -}
  u <*> Comb (Pure x) = Prod.flip Prod..@ (Prod.$) Prod..@ x <$> u
    -- & trace "Interchange Law"
  u <*> Comb (v :<*>: w) = (((Prod..) <$> u) <*> v) <*> w
    -- & trace "Composition Law"
  Comb (u :*>: v) <*> w = u *> (v <*> w)
    -- & trace "Reassociation Law 1"
  u <*> Comb (v :<*: w) = (u <*> v) <* w
    -- & trace "Reassociation Law 2"
  u <*> Comb (v :$>: x) = (u <*> pure x) <* v
    -- & trace "Reassociation Law 3"
  p <*> Comb (NegLook q) =
    (p <*> pure Prod.unit) <* negLook q
    -- & trace "Absorption Law"
  x <*> y = SimplComb
    { combData = x :<*>: y
    , combInline = False
    , combRefs = combRefs x <> combRefs y
    }

  Comb Empty *> _ = empty
    -- & trace "App Right Absorption Law"
  Comb (_ :<$>: p) *> q = p *> q
    -- & trace "Right Absorption Law"
  Comb Pure{} *> u = u
    -- & trace "Identity Law"
  Comb (u :$>: _) *> v = u *> v
    -- & trace "Identity Law"
  u *> Comb (v :*>: w) = (u *> v) *> w
    -- & trace "Associativity Law"
  x *> y = SimplComb
    { combData = x :*>: y
    , combInline = False
    , combRefs = combRefs x <> combRefs y
    }

  Comb Empty <* _ = empty
    -- & trace "App Right Absorption Law"
  u <* Comb Empty = u *> empty
    -- & trace "App Failure Weakening Law"
  p <* Comb (_ :<$>: q) = p <* q
    -- & trace "Left Absorption Law"
  u <* Comb Pure{} = u
    -- & trace "Identity Law"
  u <* Comb (v :$>: _) = u <* v
    -- & trace "Identity Law"
  Comb (u :<*: v) <* w = u <* (v <* w)
    -- & trace "Associativity Law"
  x <* y = SimplComb
    { combData = x :<*: y
    , combInline = False
    , combRefs = combRefs x <> combRefs y
    }
instance
  ( CombApplicable repr
  , CombAlternable repr
  , CombLookable repr
  , CombMatchable repr
  , CombSelectable repr
  , Eq letName
  , Hashable letName
  ) => CombApplicable (KnotComb letName repr)

-- CombFoldable
data instance Comb CombFoldable repr a where
  ChainPre :: SimplComb repr (a -> a) -> SimplComb repr a -> Comb CombFoldable repr a
  ChainPost :: SimplComb repr a -> SimplComb repr (a -> a) -> Comb CombFoldable repr a
instance CombFoldable repr => Derivable (Comb CombFoldable repr) where
  derive = \case
    ChainPre op p -> chainPre (derive op) (derive p)
    ChainPost p op -> chainPost (derive p) (derive op)
instance CombFoldable repr => CombFoldable (SimplComb repr) where
  chainPre op p = SimplComb
    { combData = ChainPre op p
    , combInline = False
    , combRefs = combRefs op <> combRefs p
    }
  chainPost p op = SimplComb
    { combData = ChainPost p op
    , combInline = False
    , combRefs = combRefs p <> combRefs op
    }
instance
  ( CombFoldable repr
  , Eq letName
  , Hashable letName
  ) => CombFoldable (KnotComb letName repr)

-- CombLookable
data instance Comb CombLookable repr a where
  Look :: SimplComb repr a -> Comb CombLookable repr a
  NegLook :: SimplComb repr a -> Comb CombLookable repr ()
  Eof :: Comb CombLookable repr ()
instance CombLookable repr => Derivable (Comb CombLookable repr) where
  derive = \case
    Look x -> look (derive x)
    NegLook x -> negLook (derive x)
    Eof -> eof
instance
  ( CombAlternable repr
  , CombApplicable repr
  , CombLookable repr
  , CombSelectable repr
  , CombMatchable repr
  ) => CombLookable (SimplComb repr) where
  look p@(Comb Pure{}) = p
    -- & trace "Pure Look Law"
  look p@(Comb Empty) = p
    -- & trace "Dead Look Law"
  look (Comb (Look x)) = look x
    -- & trace "Idempotence Law"
  look (Comb (NegLook x)) = negLook x
    -- & trace "Left Identity Law"
  look (Comb (p :$>: x)) = look p $> x
    -- & trace "Interchange Law"
  look (Comb (f :<$>: p)) = f <$> look p
    -- & trace "Interchange Law"
  look x = SimplComb
    { combData = Look x
    , combInline = False
    , combRefs = combRefs x
    }

  negLook (Comb Pure{}) = empty
    -- & trace "Pure Negative-Look"
  negLook (Comb Empty) = pure Prod.unit
    -- & trace "Dead Negative-Look"
  negLook (Comb (NegLook x)) = look (try x *> pure Prod.unit)
    -- & trace "Double Negation Law"
  negLook (Comb (Try x)) = negLook x
    -- & trace "Zero Consumption Law"
  negLook (Comb (Look x)) = negLook x
    -- & trace "Right Identity Law"
  negLook (Comb (Alt _exn (Comb (Try p)) q)) = negLook p *> negLook q
    -- FIXME: see if this really holds for all exceptions.
    -- & trace "Transparency Law"
  negLook (Comb (p :$>: _)) = negLook p
    -- & trace "NegLook Idempotence Law"
  negLook x = SimplComb
    { combData = NegLook x
    , combInline = False
    , combRefs = combRefs x
    }

  eof = SimplComb
    { combData = Eof
    , combInline = True
    , combRefs = HS.empty
    }
instance
  ( CombLookable repr
  , CombAlternable repr
  , CombApplicable repr
  , CombSelectable repr
  , CombMatchable repr
  , Eq letName
  , Hashable letName
  ) => CombLookable (KnotComb letName repr)

-- CombMatchable
data instance Comb CombMatchable repr a where
  Conditional ::
    SimplComb repr a ->
    [(Production '[] (a -> Bool), SimplComb repr b)] ->
    SimplComb repr b ->
    Comb CombMatchable repr b
instance CombMatchable repr => Derivable (Comb CombMatchable repr) where
  derive = \case
    Conditional a bs def ->
      conditional (derive a)
        ((\(p, b) -> (p, derive b)) F.<$> bs)
        (derive def)
instance
  ( CombApplicable repr
  , CombAlternable repr
  , CombLookable repr
  , CombSelectable repr
  , CombMatchable repr
  ) => CombMatchable (SimplComb repr) where
  conditional (Comb Empty) _ def = def
    -- & trace "Conditional Absorption Law"
  conditional a bs (Comb Empty)
    | Foldable.all (\case { (_, Comb Empty) -> True; _ -> False }) bs = a *> empty
      -- & trace "Conditional Weakening Law"
  conditional (Comb (Pure a)) bs def =
    Foldable.foldr (\(p, b) acc ->
      if runValue (p Prod..@ a) then b else acc
    ) def bs
    -- & trace "Conditional Pure Law"
  conditional a bs d = SimplComb
    { combData = Conditional a bs d
    , combInline = False
    , combRefs = HS.unions
        $ combRefs a
        : combRefs d
        : ((\(_p, b) -> combRefs b) F.<$> bs)
    }
instance
  ( CombMatchable repr
  , CombAlternable repr
  , CombApplicable repr
  , CombLookable repr
  , CombSelectable repr
  , Eq letName
  , Hashable letName
  ) => CombMatchable (KnotComb letName repr) where
  conditional a bs d = KnotComb
    { knotCombOpens = HM.unions
        $ knotCombOpens a
        : knotCombOpens d
        : ((\(_p, b) -> knotCombOpens b) F.<$> bs)
    , knotCombOpen = \final -> TiedComb
        { combSimpl = conditional
            (combSimpl (knotCombOpen a final))
            ((\(p, b) -> (p, combSimpl (knotCombOpen b final))) F.<$> bs)
            (combSimpl (knotCombOpen d final))
        }
    }

-- CombSatisfiable
data instance Comb (CombSatisfiable tok) repr a where
  -- | To include the @('Set' 'SomeFailure')@ is a little kludge
  -- it would be cleaner to be able to pattern-match
  -- on @(alt exn (Comb 'Satisfy'{}) (Failure{}))@
  -- when generating 'Program', but this is not easily possible then
  -- because data types have already been converted back to class methods,
  -- hence pattern-matching is no longer possible
  -- (at least not without reintroducing data types).
  SatisfyOrFail ::
    CombSatisfiable tok repr =>
    Set SomeFailure ->
    Production '[] (tok -> Bool) ->
    Comb (CombSatisfiable tok) repr tok
instance
  CombSatisfiable tok repr =>
  Derivable (Comb (CombSatisfiable tok) repr) where
  derive = \case
    SatisfyOrFail fs p -> satisfyOrFail fs p
instance
  (CombSatisfiable tok repr, Typeable tok) =>
  CombSatisfiable tok (SimplComb repr) where
  satisfyOrFail fs p = SimplComb
    { combData = SatisfyOrFail fs p
    , combInline = False -- TODO: True? depending on p?
    , combRefs = HS.empty
    }
instance
  ( CombSatisfiable tok repr
  , Typeable tok
  , Eq letName
  , Hashable letName
  ) => CombSatisfiable tok (KnotComb letName repr)

-- CombSelectable
data instance Comb CombSelectable repr a where
  Branch ::
    SimplComb repr (Either a b) ->
    SimplComb repr (a -> c) ->
    SimplComb repr (b -> c) ->
    Comb CombSelectable repr c
instance CombSelectable repr => Derivable (Comb CombSelectable repr) where
  derive = \case
    Branch lr l r -> branch (derive lr) (derive l) (derive r)
instance
  ( CombApplicable repr
  , CombAlternable repr
  , CombLookable repr
  , CombSelectable repr
  , CombMatchable repr
  ) => CombSelectable (SimplComb repr) where
  branch (Comb Empty) _ _ = empty
    -- & trace "Branch Absorption Law"
  branch b (Comb Empty) (Comb Empty) = b *> empty
    -- & trace "Branch Weakening Law"
  branch (Comb (Pure lr)) l r =
    case runValue lr of
      Left value -> l <*> pure (ProdE (Prod v c))
        where
        v = Prod.SomeData $ Prod.Var $ Identity value
        c = Prod.SomeData $ Prod.Var
          [|| case $$(runCode lr) of Left x -> x ||]
      Right value -> r <*> pure (ProdE (Prod v c))
        where
        v = Prod.SomeData $ Prod.Var $ Identity value
        c = Prod.SomeData $ Prod.Var
          [|| case $$(runCode lr) of Right x -> x ||]
    -- & trace "Branch Pure Either Law"
  branch b (Comb (Pure l)) (Comb (Pure r)) =
    ProdE (Prod v c) <$> b
    -- & trace "Branch Generalised Identity Law"
    where
    v = Prod.SomeData $ Prod.Var $ Identity $ either (runValue l) (runValue r)
    c = Prod.SomeData $ Prod.Var [|| either $$(runCode l) $$(runCode r) ||]
  branch (Comb (x :*>: y)) p q = x *> branch y p q
    -- & trace "Interchange Law"
  branch b l (Comb Empty) =
    branch (pure (ProdE (Prod v c)) <*> b) empty l
    -- & trace "Negated Branch Law"
    where
    v = Prod.SomeData $ Prod.Var $ Identity $ either Right Left
    c = Prod.SomeData $ Prod.Var $ [||either Right Left||]
  branch (Comb (Branch b (Comb Empty) (Comb (Pure lr)))) (Comb Empty) br =
    branch (pure (ProdE (Prod v c)) <*> b) empty br
    -- & trace "Branch Fusion Law"
    where
    v = Prod.SomeData $ Prod.Var $ Identity $ \case
      Left{} -> Left ()
      Right r ->
        case runValue lr r of
          Left{} -> Left ()
          Right rr -> Right rr
    c = Prod.SomeData $ Prod.Var
      [|| \case Left{} -> Left ()
                Right r -> case $$(runCode lr) r of
                             Left{} -> Left ()
                             Right rr -> Right rr ||]
  branch b l r = SimplComb
    { combData = Branch b l r
    , combInline = False
    , combRefs = HS.unions [ combRefs b, combRefs l, combRefs r ]
    }
instance
  ( CombSelectable repr
  , CombAlternable repr
  , CombApplicable repr
  , CombLookable repr
  , CombMatchable repr
  , Eq letName
  , Hashable letName
  ) => CombSelectable (KnotComb letName repr)

-- CombRegisterableUnscoped
data instance Comb CombRegisterableUnscoped repr a where
  NewUnscoped :: UnscopedRegister a -> SimplComb repr a -> SimplComb repr b -> Comb CombRegisterableUnscoped repr b
  GetUnscoped :: UnscopedRegister a -> Comb CombRegisterableUnscoped repr a
  PutUnscoped :: UnscopedRegister a -> SimplComb repr a -> Comb CombRegisterableUnscoped repr ()
instance CombRegisterableUnscoped repr => Derivable (Comb CombRegisterableUnscoped repr) where
  derive = \case
    NewUnscoped r ini x -> newUnscoped r (derive ini) (derive x)
    GetUnscoped r -> getUnscoped r
    PutUnscoped r x -> putUnscoped r (derive x)
instance -- TODO: optimizations
  ( CombRegisterableUnscoped repr
  ) => CombRegisterableUnscoped (SimplComb repr) where
  newUnscoped r ini x = SimplComb
    { combData = NewUnscoped r ini x
    , combInline = combInline ini && combInline x
    , combRefs = combRefs ini <> combRefs x
    }
  getUnscoped r = SimplComb
    { combData = GetUnscoped r
    , combInline = True
    , combRefs = HS.empty
    }
  putUnscoped r x = SimplComb
    { combData = PutUnscoped r x
    , combInline = combInline x
    , combRefs = combRefs x
    }
instance
  ( CombRegisterableUnscoped repr
  , Eq letName
  , Hashable letName
  ) => CombRegisterableUnscoped (KnotComb letName repr) where

-- Letsable
data instance Comb (Letsable letName) repr a where
  Lets ::
    LetBindings letName (SimplComb repr) ->
    SimplComb repr a ->
    Comb (Letsable letName) repr a
instance
  Letsable letName repr =>
  Derivable (Comb (Letsable letName) repr) where
  derive = \case
    Lets defs x -> lets
      ((\(SomeLet sub) -> SomeLet (derive sub)) F.<$> defs)
      (derive x)
instance
  (Letsable letName repr, Typeable letName) =>
  Letsable letName (SimplComb repr) where
  lets defs body = SimplComb
    { combData = Lets defs body
    , combInline = False
    , combRefs = HS.unions
        $ combRefs body
        : ((\(SomeLet sub) -> combRefs sub) F.<$> HM.elems defs)
    }
instance
  Letsable TH.Name repr =>
  Letsable TH.Name (KnotComb TH.Name repr) where
  lets defs body = KnotComb
    { knotCombOpens =
        HM.unions
          $ knotCombOpens body
          : ((\(SomeLet sub) -> SomeLet . knotCombOpen sub) F.<$> defs)
          -- Not really necessary to include 'knotCombOpens' of 'defs' here
          -- since there is only a single 'lets' at the top of the AST,
          -- but well.
          : ((\(SomeLet sub) -> knotCombOpens sub) F.<$> HM.elems defs)
    , knotCombOpen = \final -> TiedComb
        { combSimpl =
          let bodySimpl = combSimpl $ knotCombOpen body final in
          let defsSimpl = (\(SomeLet sub) -> SomeLet $ combSimpl $ knotCombOpen sub final) F.<$> defs in
          let defsUsed = HS.unions
                $ combRefs bodySimpl
                : ((\(SomeLet sub) -> combRefs sub) F.<$> HM.elems defsSimpl) in
          lets (HM.intersection defsSimpl (HS.toMap defsUsed)) bodySimpl
        }
    }

-- Referenceable
data instance Comb (Referenceable letName) repr a where
  Ref :: Bool -> letName -> Comb (Referenceable letName) repr a
instance
  Referenceable letName repr =>
  Derivable (Comb (Referenceable letName) repr) where
  derive = \case
    Ref isRec name -> ref isRec name
instance
  Referenceable TH.Name repr =>
  Referenceable TH.Name (SimplComb repr) where
  ref isRec name = SimplComb
    { combData = Ref isRec name
    , combInline = not isRec
    , combRefs = HS.singleton name
    }
instance
  Referenceable TH.Name repr =>
  Referenceable TH.Name (KnotComb TH.Name repr) where
  ref isRec name = KnotComb
    { knotCombOpens = HM.empty
    , knotCombOpen = \final ->
      if isRec
      then TiedComb
        { combSimpl = ref isRec name
        }
      else case final HM.! name of
        SomeLet a@TiedComb
          { combSimpl = p@SimplComb{ combInline = True }
          } -> a{combSimpl = unsafeSimplComb p}
        SomeLet TiedComb
          { combSimpl = SimplComb{ combRefs = refs }
          } -> TiedComb
            { combSimpl = (ref isRec name)
              { combRefs = HS.insert name refs }
            }
    }