{-# LANGUAGE OverloadedStrings #-}

-- | This module exports functionality for generating a call graph of
-- an Futhark program.
module Futhark.Analysis.CallGraph
  ( CallGraph,
    buildCallGraph,
    isFunInCallGraph,
    calls,
    calledByConsts,
    allCalledBy,
    numOccurences,
    findNoninlined,
  )
where

import Control.Monad.Writer.Strict
import Data.List (foldl')
import qualified Data.Map.Strict as M
import Data.Maybe (isJust)
import qualified Data.Set as S
import Futhark.IR.SOACS
import Futhark.Util.Pretty

type FunctionTable = M.Map Name (FunDef SOACS)

buildFunctionTable :: Prog SOACS -> FunctionTable
buildFunctionTable = foldl expand M.empty . progFuns
  where
    expand ftab f = M.insert (funDefName f) f ftab

-- | A unique (at least within a function) name identifying a function
-- call.  In practice the first element of the corresponding pattern.
type CallId = VName

data FunCalls = FunCalls
  { fcMap :: M.Map CallId (Attrs, Name),
    fcAllCalled :: S.Set Name
  }
  deriving (Eq, Ord, Show)

instance Monoid FunCalls where
  mempty = FunCalls mempty mempty

instance Semigroup FunCalls where
  FunCalls x1 y1 <> FunCalls x2 y2 = FunCalls (x1 <> x2) (y1 <> y2)

fcCalled :: Name -> FunCalls -> Bool
fcCalled f fcs = f `S.member` fcAllCalled fcs

type FunGraph = M.Map Name FunCalls

-- | The call graph is a mapping from a function name, i.e., the
-- caller, to a record of the names of functions called *directly* (not
-- transitively!) by the function.
--
-- We keep track separately of the functions called by constants.
data CallGraph = CallGraph
  { cgCalledByFuns :: FunGraph,
    cgCalledByConsts :: FunCalls
  }
  deriving (Eq, Ord, Show)

-- | Is the given function known to the call graph?
isFunInCallGraph :: Name -> CallGraph -> Bool
isFunInCallGraph f = M.member f . cgCalledByFuns

-- | Does the first function call the second?
calls :: Name -> Name -> CallGraph -> Bool
calls caller callee =
  maybe False (fcCalled callee) . M.lookup caller . cgCalledByFuns

-- | Is the function called in any of the constants?
calledByConsts :: Name -> CallGraph -> Bool
calledByConsts callee = fcCalled callee . cgCalledByConsts

-- | All functions called by this function.
allCalledBy :: Name -> CallGraph -> S.Set Name
allCalledBy f = maybe mempty fcAllCalled . M.lookup f . cgCalledByFuns

-- | @buildCallGraph prog@ build the program's call graph.
buildCallGraph :: Prog SOACS -> CallGraph
buildCallGraph prog =
  CallGraph fg cg
  where
    fg = foldl' (buildFGfun ftable) M.empty entry_points
    cg = buildFGStms $ progConsts prog

    entry_points =
      S.fromList (map funDefName (filter (isJust . funDefEntryPoint) $ progFuns prog))
        <> fcAllCalled cg
    ftable = buildFunctionTable prog

count :: Ord k => [k] -> M.Map k Int
count ks = M.fromListWith (+) $ zip ks $ repeat 1

-- | Produce a mapping of the number of occurences in the call graph
-- of each function.  Only counts functions that are called at least
-- once.
numOccurences :: CallGraph -> M.Map Name Int
numOccurences (CallGraph funs consts) =
  count $ map snd $ M.elems (fcMap consts <> foldMap fcMap (M.elems funs))

-- | @buildCallGraph ftable fg fname@ updates @fg@ with the
-- contributions of function @fname@.
buildFGfun :: FunctionTable -> FunGraph -> Name -> FunGraph
buildFGfun ftable fg fname =
  -- Check if function is a non-builtin that we have not already
  -- processed.
  case M.lookup fname ftable of
    Just f | Nothing <- M.lookup fname fg -> do
      let callees = buildFGBody $ funDefBody f
          fg' = M.insert fname callees fg
      -- recursively build the callees
      foldl' (buildFGfun ftable) fg' $ fcAllCalled callees
    _ -> fg

buildFGStms :: Stms SOACS -> FunCalls
buildFGStms = mconcat . map buildFGstm . stmsToList

buildFGBody :: Body -> FunCalls
buildFGBody = buildFGStms . bodyStms

buildFGstm :: Stm -> FunCalls
buildFGstm (Let (Pat (p : _)) aux (Apply fname _ _ _)) =
  FunCalls (M.singleton (patElemName p) (stmAuxAttrs aux, fname)) (S.singleton fname)
buildFGstm (Let _ _ (Op op)) = execWriter $ mapSOACM folder op
  where
    folder =
      identitySOACMapper
        { mapOnSOACLambda = \lam -> do
            tell $ buildFGBody $ lambdaBody lam
            return lam
        }
buildFGstm (Let _ _ e) = execWriter $ mapExpM folder e
  where
    folder =
      identityMapper
        { mapOnBody = \_ body -> do
            tell $ buildFGBody body
            return body
        }

-- | The set of all functions that are called noinline somewhere, or
-- have a noinline attribute on their definition.
findNoninlined :: Prog SOACS -> S.Set Name
findNoninlined prog =
  foldMap noinlineDef (progFuns prog)
    <> foldMap onStm (foldMap (bodyStms . funDefBody) (progFuns prog) <> progConsts prog)
  where
    onStm :: Stm -> S.Set Name
    onStm (Let _ aux (Apply fname _ _ _))
      | "noinline" `inAttrs` stmAuxAttrs aux =
        S.singleton fname
    onStm (Let _ _ e) = execWriter $ mapExpM folder e
      where
        folder =
          identityMapper
            { mapOnBody = \_ body -> do
                tell $ foldMap onStm $ bodyStms body
                return body,
              mapOnOp =
                mapSOACM
                  identitySOACMapper
                    { mapOnSOACLambda = \lam -> do
                        tell $ foldMap onStm $ bodyStms $ lambdaBody lam
                        return lam
                    }
            }

    noinlineDef fd
      | "noinline" `inAttrs` funDefAttrs fd =
        S.singleton $ funDefName fd
      | otherwise =
        mempty

instance Pretty FunCalls where
  ppr = stack . map f . M.toList . fcMap
    where
      f (x, (attrs, y)) = "=>" <+> ppr y <+> parens ("at" <+> ppr x <+> ppr attrs)

instance Pretty CallGraph where
  ppr (CallGraph fg cg) =
    stack $
      punctuate line $
        ppFunCalls ("called at top level", cg) : map ppFunCalls (M.toList fg)
    where
      ppFunCalls (f, fcalls) =
        ppr f </> text (map (const '=') (nameToString f))
          </> indent 2 (ppr fcalls)
