packages feed

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

{-# LANGUAGE DeriveAnyClass        #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE StandaloneDeriving    #-}

module Language.Cimple.Analysis.NullabilityAnalysis
    ( NullabilityFacts
    , NullabilityResult (..)
    , runNullabilityAnalysis
    ) where

import           Control.Monad.Identity            (Identity (..))
import           Data.Aeson                        (ToJSON)
import           Data.Fix                          (Fix (..), foldFix, unFix)
import           Data.Foldable                     (foldMap, toList)
import           Data.List                         (foldl')
import           Data.Map.Strict                   (Map)
import qualified Data.Map.Strict                   as Map
import           Data.Maybe                        (fromMaybe, mapMaybe)
import           Data.Set                          (Set)
import qualified Data.Set                          as Set
import           Data.Text                         (Text)
import qualified Data.Text                         as T
import qualified Debug.Trace                       as Debug
import           GHC.Generics                      (Generic)
import           Language.Cimple                   (NodeF (..))
import qualified Language.Cimple                   as C
import           Language.Cimple.Analysis.AstUtils (getAlexPosn, getParamName,
                                                    getVar, isNonnullParam,
                                                    isNonnullType)
import           Language.Cimple.Analysis.DataFlow (CFG, CFGNode (..),
                                                    DataFlow (..), buildCFG,
                                                    fixpoint)
import qualified Language.Cimple.Program           as Program

-- | Variables currently known to be non-null.
type NullabilityFacts = Set Text

data NullabilityResult = NullabilityResult
    { nrFunctionFacts  :: Map Text (Map Int NullabilityFacts)
    -- ^ FunctionName -> (CFGNodeID -> Facts)
    , nrStatementFacts :: Map Text (Map C.AlexPosn NullabilityFacts)
    -- ^ FunctionName -> (Position -> Facts)
    } deriving (Show, Generic, ToJSON)

debugging :: Bool
debugging = False

dtrace :: String -> a -> a
dtrace msg x = if debugging then Debug.trace msg x else x

data NullabilityContext l = NullabilityContext
    { ncNonnullParams :: NonnullParams
    }

instance DataFlow Identity NullabilityContext Text NullabilityFacts () where
    emptyFacts _ = return Set.empty
    join _ a b = return $ Set.intersection a b
    transfer ctx _ _ facts stmt = return (transferStmt (ncNonnullParams ctx) facts stmt, Set.empty)

type NonnullParams = Map Text (Set Int)

buildNonnullParamsMap :: Program.Program Text -> NonnullParams
buildNonnullParamsMap program =
    Map.fromList $ concatMap (collectPrototypes . snd) (Program.toList program)
  where
    collectPrototypes nodes = concatMap collectPrototypes' nodes
    collectPrototypes' (Fix node) =
        let rest = concatMap collectPrototypes' (toList node)
        in case node of
            C.FunctionPrototype _ (C.L _ _ name) params ->
                let nonnulls = Set.fromList [ i | (i, p) <- zip [0..] params, isNonnullParam p ]
                in (name, nonnulls) : rest
            _ -> rest

runNullabilityAnalysis :: Program.Program Text -> NullabilityResult
runNullabilityAnalysis program =
    let allNodes = Program.toList program >>= snd
        nnMap = buildNonnullParamsMap program
        results = concatMap (collectFunctions nnMap) allNodes
        funcFacts = Map.fromList $ map (\(n, f, _) -> (n, f)) results
        stmtFacts = Map.fromList $ map (\(n, _, s) -> (n, s)) results
    in NullabilityResult funcFacts stmtFacts
  where
    collectFunctions :: NonnullParams -> C.Node (C.Lexeme Text) -> [(Text, Map Int NullabilityFacts, Map C.AlexPosn NullabilityFacts)]
    collectFunctions nnMap n@(Fix node) =
        let results = concatMap (collectFunctions nnMap) (toList node)
            current = case node of
                C.FunctionDefn _ proto _ ->
                    case getProtoName proto of
                        Just name ->
                            let initialFacts = getInitialFacts proto
                                ctx = NullabilityContext nnMap
                                cfg = runIdentity $ buildCFG ctx n initialFacts :: CFG Text NullabilityFacts
                                (finalCfg, _) = runIdentity $ fixpoint ctx name cfg
                                facts = Map.map cfgInFacts finalCfg
                                sFacts = Map.unions $ map (computeNodeStatementFacts nnMap name) (Map.elems finalCfg)
                            in [(name, facts, sFacts)]
                        Nothing -> []
                _ -> []
        in current ++ results

    getInitialFacts (Fix (C.FunctionPrototype _ _ params)) =
        Set.fromList $ mapMaybe (\p -> if isNonnullParam p then getParamName p else Nothing) params
    getInitialFacts (Fix (C.Commented _ n)) = getInitialFacts n
    getInitialFacts _ = Set.empty

    getProtoName (Fix (C.FunctionPrototype _ (C.L _ _ name) _)) = Just name
    getProtoName (Fix (C.Commented _ n))                        = getProtoName n
    getProtoName _                                              = Nothing

    computeNodeStatementFacts nnMap funcName node =
        snd $ foldl' (\(facts, acc) stmt ->
            let newFacts = transferStmt nnMap facts stmt
                acc' = case getAlexPosn stmt of
                         Just pos ->
                             dtrace ("Nullability STORE: " ++ T.unpack funcName ++ " at " ++ show pos ++ " facts=" ++ show facts) $
                             Map.insert pos facts acc -- Facts BEFORE statement
                         Nothing  -> acc
            in (newFacts, acc')
        ) (cfgInFacts node, Map.empty) (cfgStmts node)

transferStmt :: NonnullParams -> NullabilityFacts -> C.Node (C.Lexeme Text) -> NullabilityFacts
transferStmt nnMap facts stmt@(Fix node) =
    let implicit = extractImplicitNonnull stmt
        facts' = facts <> implicit
        newFacts = case node of
            C.VarDeclStmt (Fix (C.VarDecl _ (C.L _ _ name) _)) mInit ->
                case mInit of
                    Just initExpr -> if isGuaranteedNonnull facts' initExpr
                                     then Set.insert name facts'
                                     else Set.delete name facts'
                    Nothing -> facts'

            C.ExprStmt (Fix (C.AssignExpr (Fix (C.VarExpr (C.L _ _ name))) _ rhs)) ->
                if isGuaranteedNonnull facts' rhs
                then Set.insert name facts'
                else Set.delete name facts'

            C.ExprStmt (Fix (C.FunctionCall (Fix (C.VarExpr (C.L _ _ "__tokstyle_assume_true"))) [cond])) ->
                facts' <> extractVarsFromNonnullCond cond

            C.ExprStmt (Fix (C.FunctionCall (Fix (C.VarExpr (C.L _ _ "__tokstyle_assume_false"))) [cond])) ->
                facts' <> extractVarsFromNullCond cond

            C.ExprStmt (Fix (C.FunctionCall (Fix (C.VarExpr (C.L _ _ name))) args)) ->
                case Map.lookup name nnMap of
                    Just nnIndices ->
                        let calledWithNonnull = Set.fromList [ var | (i, arg) <- zip [0..] args, Set.member i nnIndices, Just var <- [getVar arg] ]
                        in facts' <> calledWithNonnull
                    Nothing -> facts'

            _ -> facts'
    in dtrace ("Nullability TRANSFER: " ++ show (getAlexPosn stmt) ++ " facts=" ++ show facts ++ " -> " ++ show newFacts) newFacts

extractImplicitNonnull :: C.Node (C.Lexeme Text) -> Set Text
extractImplicitNonnull (Fix node) =
    let self = case node of
            C.UnaryExpr C.UopDeref e -> maybe Set.empty Set.singleton (getVar e)
            C.MemberAccess e _ -> maybe Set.empty Set.singleton (getVar e)
            C.PointerAccess e _ -> maybe Set.empty Set.singleton (getVar e)
            C.ArrayAccess e _ -> maybe Set.empty Set.singleton (getVar e)
            C.CastExpr ty e | isNonnullType ty -> maybe Set.empty Set.singleton (getVar e)
            _ -> Set.empty
    in self <> foldMap extractImplicitNonnull node


isGuaranteedNonnull :: NullabilityFacts -> C.Node (C.Lexeme Text) -> Bool
isGuaranteedNonnull facts (Fix node) = case node of
    C.VarExpr (C.L _ _ name) -> Set.member name facts
    C.LiteralExpr C.String _ -> True
    C.UnaryExpr C.UopAddress _ -> True
    C.ParenExpr e -> isGuaranteedNonnull facts e
    C.CastExpr ty e -> isNonnullType ty || isGuaranteedNonnull facts e
    C.FunctionCall (Fix (C.VarExpr (C.L _ _ "malloc"))) _ -> True
    C.FunctionCall (Fix (C.VarExpr (C.L _ _ "realloc"))) _ -> True
    C.LiteralExpr C.ConstId (C.L _ _ "nullptr") -> False
    C.LiteralExpr C.Int (C.L _ _ "0") -> False
    _ -> False

-- | Extract variable names from a condition that, if true, implies the variables are non-null.
extractVarsFromNonnullCond :: C.Node (C.Lexeme Text) -> Set Text
extractVarsFromNonnullCond (Fix node) = case node of
    C.VarExpr (C.L _ _ name) -> Set.singleton name
    C.ParenExpr e -> extractVarsFromNonnullCond e
    C.BinaryExpr lhs C.BopAnd rhs ->
        extractVarsFromNonnullCond lhs <> extractVarsFromNonnullCond rhs
    C.BinaryExpr lhs C.BopNe rhs ->
        case (unFix lhs, unFix rhs) of
            (C.VarExpr (C.L _ _ v), C.LiteralExpr C.ConstId (C.L _ _ "nullptr")) -> Set.singleton v
            (C.LiteralExpr C.ConstId (C.L _ _ "nullptr"), C.VarExpr (C.L _ _ v)) -> Set.singleton v
            (C.VarExpr (C.L _ _ v), C.LiteralExpr C.Int (C.L _ _ "0"))           -> Set.singleton v
            (C.LiteralExpr C.Int (C.L _ _ "0"), C.VarExpr (C.L _ _ v))           -> Set.singleton v
            _ -> Set.empty
    _ -> Set.empty

-- | Extract variable names from a condition that, if false, implies the variables are non-null.
extractVarsFromNullCond :: C.Node (C.Lexeme Text) -> Set Text
extractVarsFromNullCond (Fix node) = case node of
    C.UnaryExpr C.UopNot e -> extractVarsFromNonnullCond e
    C.ParenExpr e -> extractVarsFromNullCond e
    C.BinaryExpr lhs C.BopOr rhs ->
        extractVarsFromNullCond lhs <> extractVarsFromNullCond rhs
    C.BinaryExpr lhs C.BopEq rhs ->
        case (unFix lhs, unFix rhs) of
            (C.VarExpr (C.L _ _ v), C.LiteralExpr C.ConstId (C.L _ _ "nullptr")) -> Set.singleton v
            (C.LiteralExpr C.ConstId (C.L _ _ "nullptr"), C.VarExpr (C.L _ _ v)) -> Set.singleton v
            (C.VarExpr (C.L _ _ v), C.LiteralExpr C.Int (C.L _ _ "0"))           -> Set.singleton v
            (C.LiteralExpr C.Int (C.L _ _ "0"), C.VarExpr (C.L _ _ v))           -> Set.singleton v
            _ -> Set.empty
    _ -> Set.empty