{-# LANGUAGE LambdaCase #-}

module Clash.Core.Termination
  ( RecInfo
  , mkRecInfo
  , isRecursive
  , recursiveGroup
  ) where

import Control.Lens.Fold
import Data.Graph (SCC(..))
import qualified Data.Graph as Graph
import qualified Data.List as List

import Clash.Core.FreeVars
import Clash.Core.Var
import Clash.Core.VarEnv
import Clash.Driver.Types

-- Quick lookup for whether a binding is recursive or non-recursive. If a
-- binding is non-recursive, we can assume that it terminates and skip
-- analysing it.
--
data RecInfo = RecInfo
  { recBindings    :: [VarSet]
    -- ^ Recursive bindings, organized into groups of strongly connected
    -- components.
  , nonRecBindings :: VarSet
    -- ^ Non-recursive bindings
  }

instance Show RecInfo where
  show (RecInfo rs ns) =
    "recursive groups:\n" <> show (fmap eltsVarSet rs)
      <> "\n\nnon-recursive:\n" <> show (eltsVarSet ns)

instance Semigroup RecInfo where
  {-# INLINE (<>) #-}
  RecInfo rX nX <> RecInfo rY nY =
    RecInfo (rX <> rY) (nX <> nY)

instance Monoid RecInfo where
  {-# INLINE mempty #-}
  mempty = RecInfo mempty mempty

  {-# INLINE mappend #-}
  mappend = (<>)

-- | Given a map of top-level bindings, identify which terms are recursive and
-- organize them into groups of mutually recursive bindings. For example,
-- calling mkRecInfo on a BindingMap with the definitions
--
--   f []     = []
--   f (x:xs) = g x : h xs
--
--   g x = x + 1
--
--   h []     = []
--   h (x:xs) = x : f xs
--
--   i []     = []
--   i (x:xs) = x * 2 : i xs
--
-- would identify [f, g] and [i] as recursive groups, and g as non-recursive.
--
mkRecInfo :: BindingMap -> RecInfo
mkRecInfo =
  mconcat . fmap asInfo . dependencies
 where
  -- Convert a SCC to RecInfo
  asInfo = \case
    AcyclicSCC x -> RecInfo [] (unitVarSet $ bindingId x)
    CyclicSCC xs -> RecInfo [mkVarSet $ fmap bindingId xs] emptyVarSet

  -- Get the SCCs of the dependency graph of free variables.
  dependencies =
    Graph.stronglyConnComp . eltsVarEnv . fmap go
   where
    go x = let fvs = bindingTerm x ^.. freeIds
            in (x, bindingId x, fvs)

-- | Check if a global binder is recursive. To be conservative, binders which
-- are not included in the RecInfo are assumed to be recursive.
--
isRecursive :: Id -> RecInfo -> Bool
isRecursive i
  | isGlobalId i = not . elemVarSet i . nonRecBindings
  | otherwise = error ("isRecursive: " <> show i <> " is not a global Id")

-- | Return the recursive group that a global binder belongs to. If the
-- binder is non-recursive or not included in the RecInfo, Nothing is returned.
--
recursiveGroup :: Id -> RecInfo -> Maybe VarSet
recursiveGroup i = List.find (elemVarSet i) . recBindings

