packages feed

hic-0.0.0.1: src/Language/Cimple/Analysis/CallGraphAnalysis.hs

{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
module Language.Cimple.Analysis.CallGraphAnalysis
    ( CallGraphResult (..)
    , CallGraph
    , SccType (..)
    , runCallGraphAnalysis
    ) where

import           Control.Monad.State.Strict (State, execState)
import qualified Control.Monad.State.Strict as State
import           Data.Aeson                 (ToJSON)
import           Data.Fix                   (Fix (..), foldFix)
import           Data.Graph                 (SCC (..), stronglyConnComp)
import           Data.Map.Strict            (Map)
import qualified Data.Map.Strict            as Map
import           Data.Set                   (Set)
import qualified Data.Set                   as Set
import           Data.Text                  (Text)
import           GHC.Generics               (Generic)
import           Language.Cimple            (Lexeme (..), Node, NodeF (..))
import qualified Language.Cimple            as C
import qualified Language.Cimple.Program    as Program

type CallGraph = Map Text (Set Text)

data SccType = Acyclic Text | Cyclic [Text]
    deriving (Show, Eq, Generic)

instance ToJSON SccType

data CallGraphResult = CallGraphResult
    { cgrDirectCalls :: CallGraph
    , cgrSccs        :: [SccType]
    } deriving (Show, Generic)

instance ToJSON CallGraphResult

data AnalysisState = AnalysisState
    { asCurrentFunc :: Maybe Text
    , asCalls       :: CallGraph
    , asLocalVars   :: Set Text
    }

runCallGraphAnalysis :: Program.Program Text -> CallGraphResult
runCallGraphAnalysis program =
    let initialState = AnalysisState Nothing Map.empty Set.empty
        finalState = execState (mapM_ (mapM_ traverseNode . snd) (Program.toList program)) initialState
        calls = asCalls finalState
        -- Convert Map to adjacency list for Data.Graph.stronglyConnComp
        -- Triple: (node_value, key, [callees])
        adjacencyList = [ (name, name, Set.toList callees) | (name, callees) <- Map.toList calls ]
        sccs = map fromSCC $ stronglyConnComp adjacencyList
    in CallGraphResult calls sccs
  where
    fromSCC (AcyclicSCC node) = Acyclic node
    fromSCC (CyclicSCC nodes) = Cyclic nodes

    traverseNode = snd . foldFix alg
      where
        alg f = (Fix (fmap fst f), case f of
            C.FunctionDefn _ (protoOrig, _) (_, bodyAction) -> do
                case unFix protoOrig of
                    C.FunctionPrototype _ (L _ _ name) params -> do
                        oldFunc <- State.gets asCurrentFunc
                        oldVars <- State.gets asLocalVars
                        State.modify $ \s -> s { asCurrentFunc = Just name, asLocalVars = Set.empty }
                        mapM_ registerParam params
                        -- Ensure the function exists in the map even if it calls nothing
                        State.modify $ \s -> s { asCalls = Map.insertWith Set.union name Set.empty (asCalls s) }
                        bodyAction
                        State.modify $ \s -> s { asCurrentFunc = oldFunc, asLocalVars = oldVars }
                    _ -> bodyAction

            C.VarDeclStmt (declOrig, _) mInit -> do
                case unFix declOrig of
                    C.VarDecl _ (L _ _ name) _ ->
                        State.modify $ \s -> s { asLocalVars = Set.insert name (asLocalVars s) }
                    _ -> return ()
                mapM_ snd mInit

            C.FunctionCall (funOrig, _) args -> do
                case unFix funOrig of
                    C.VarExpr (L _ _ callee) -> do
                        locals <- State.gets asLocalVars
                        mCaller <- State.gets asCurrentFunc
                        case mCaller of
                            Just caller | not (Set.member callee locals) -> State.modify $ \s ->
                                let calls = Map.insertWith Set.union caller (Set.singleton callee) (asCalls s)
                                    calls' = Map.insertWith Set.union callee Set.empty calls
                                in s { asCalls = calls' }
                            _ -> return ()
                    _ -> return ()
                mapM_ snd args

            node -> sequence_ (fmap snd node))

    registerParam = snd . foldFix alg'
      where
        alg' f = (Fix (fmap fst f), case f of
            C.VarDecl _ (L _ _ name) _ ->
                State.modify $ \s -> s { asLocalVars = Set.insert name (asLocalVars s) }
            C.NonNullParam (_, action) -> action
            C.NullableParam (_, action) -> action
            node -> sequence_ (fmap snd node))