------------------------------------------------------------------------------
--- Determinism analysis:
--- checks whether functions are deterministic or nondeterministic, i.e.,
--- whether its evaluation on ground argument terms might cause
--- different computation paths.
---
--- @author Michael Hanus
--- @version June 2022
------------------------------------------------------------------------------
module Analysis.Deterministic
( overlapAnalysis, showOverlap, showDet
, functionalAnalysis, showFunctional, isNondetDefined
, Deterministic(..),nondetAnalysis
, showNonDetDeps, nondetDepAnalysis, nondetDepAllAnalysis
) where
import Analysis.Types
import FlatCurry.Types
import FlatCurry.Goodies
import Data.List
import Data.Char (isDigit)
------------------------------------------------------------------------------
-- The overlapping analysis can be applied to individual functions.
-- It assigns to a FlatCurry function definition a flag which is True
-- if this function is defined with overlapping left-hand sides.
overlapAnalysis :: Analysis Bool
overlapAnalysis = simpleFuncAnalysis "Overlapping" isOverlappingFunction
isOverlappingFunction :: FuncDecl -> Bool
isOverlappingFunction (Func _ _ _ _ (Rule _ e)) = orInExpr e
isOverlappingFunction (Func f _ _ _ (External _)) = f == ("Prelude","?")
-- Check an expression for occurrences of OR:
orInExpr :: Expr -> Bool
orInExpr (Var _) = False
orInExpr (Lit _) = False
orInExpr (Comb _ f es) = f == (pre "?") || any orInExpr es
orInExpr (Free _ e) = orInExpr e
orInExpr (Let bs e) = any orInExpr (map snd bs) || orInExpr e
orInExpr (Or _ _) = True
orInExpr (Case _ e bs) = orInExpr e || any orInBranch bs
where orInBranch (Branch _ be) = orInExpr be
orInExpr (Typed e _) = orInExpr e
-- Show overlapping information as a string.
showOverlap :: AOutFormat -> Bool -> String
showOverlap _ True = "overlapping"
showOverlap AText False = "non-overlapping"
showOverlap ANote False = ""
------------------------------------------------------------------------------
-- The functional analysis is a global function dependency analysis.
-- It assigns to a FlatCurry function definition a flag which is True
-- if this function is purely functional defined, i.e., its definition
-- does not depend on operation containing overlapping rules or free variables.
functionalAnalysis :: Analysis Bool
functionalAnalysis = dependencyFuncAnalysis "Functional" True isFuncDefined
-- Show functionally defined information as a string.
showFunctional :: AOutFormat -> Bool -> String
showFunctional _ True = "functional"
showFunctional AText False = "defined with logic features"
showFunctional ANote False = ""
-- An operation is functionally defined if its definition is not
-- non-deterministic (no overlapping rules, no extra variables) and
-- it depends only on functionally defined operations.
isFuncDefined :: FuncDecl -> [(QName,Bool)] -> Bool
isFuncDefined func calledFuncs =
not (isNondetDefined func) && all snd calledFuncs
-- Is a function f defined to be potentially non-deterministic, i.e.,
-- is the rule non-deterministic or does it contain extra variables?
isNondetDefined :: FuncDecl -> Bool
isNondetDefined (Func f _ _ _ rule) =
f `notElem` (map pre ["failed","$!!","$##","normalForm","groundNormalForm"])
-- these operations are internally defined in PAKCS with extra variables
&& isNondetRule rule
where
isNondetRule (Rule _ e) = orInExpr e || extraVarInExpr e
isNondetRule (External _) = f==("Prelude","?")
-- check an expression for occurrences of extra variables:
extraVarInExpr :: Expr -> Bool
extraVarInExpr (Var _) = False
extraVarInExpr (Lit _) = False
extraVarInExpr (Comb _ _ es) = or (map extraVarInExpr es)
extraVarInExpr (Free vars e) = (not (null vars)) || extraVarInExpr e
extraVarInExpr (Let bs e) = any extraVarInExpr (map snd bs) || extraVarInExpr e
extraVarInExpr (Or e1 e2) = extraVarInExpr e1 || extraVarInExpr e2
extraVarInExpr (Case _ e bs) = extraVarInExpr e || any extraVarInBranch bs
where extraVarInBranch (Branch _ be) = extraVarInExpr be
extraVarInExpr (Typed e _) = extraVarInExpr e
------------------------------------------------------------------------------
-- The determinism analysis is a global function dependency analysis.
-- It assigns to a function a flag which indicates whether is function
-- might be non-deterministic, i.e., might reduce in different ways
-- for given ground arguments.
-- If the non-determinism is encapsulated (set functions, getAllValues),
-- it is classified as deterministic.
--- Data type to represent determinism information.
data Deterministic = NDet | Det
deriving (Eq, Ord, Show, Read)
-- Show determinism information as a string.
showDet :: AOutFormat -> Deterministic -> String
showDet _ NDet = "non-deterministic"
showDet AText Det = "deterministic"
showDet ANote Det = ""
nondetAnalysis :: Analysis Deterministic
nondetAnalysis = dependencyFuncAnalysis "Deterministic" Det nondetFunc
-- An operation is non-deterministic if its definition is potentially
-- non-deterministic or it calls a non-deterministic operation
-- where the non-deterministic call is not encapsulated.
nondetFunc :: FuncDecl -> [(QName,Deterministic)] -> Deterministic
nondetFunc func@(Func _ _ _ _ rule) calledFuncs =
if isNondetDefined func || callsNDOpInRule rule
then NDet
else Det
where
callsNDOpInRule (Rule _ e) = callsNDOp e
callsNDOpInRule (External _) = False
callsNDOp (Var _) = False
callsNDOp (Lit _) = False
callsNDOp (Free _ e) = callsNDOp e
callsNDOp (Let bs e) = any callsNDOp (map snd bs) || callsNDOp e
callsNDOp (Or _ _) = True
callsNDOp (Case _ e bs) =
callsNDOp e || any (\ (Branch _ be) -> callsNDOp be) bs
callsNDOp (Typed e _) = callsNDOp e
callsNDOp (Comb _ qf@(mn,fn) es)
| mn == "SetFunctions" && take 3 fn == "set" && all isDigit (drop 3 fn)
= -- non-determinism of function (first argument) is encapsulated so that
-- its called ND functions are not relevant:
if null es then False -- this case should not occur
else any callsNDOp (tail es)
| isStrongEncapsOp qf
= -- non-determinism of argument is encapsulated so that
-- its called ND functions are not relevant:
False
| otherwise
= maybe False (==NDet) (lookup qf calledFuncs) || any callsNDOp es
-- Does the operation ensures the strong encapsulation of its argument?
isStrongEncapsOp :: QName -> Bool
isStrongEncapsOp (mn,_) =
mn `elem` ["Control.AllSolutions", "Control.AllValues"]
------------------------------------------------------------------------------
--- Data type to represent information about non-deterministic dependencies.
--- Basically, it is the set (represented as a sorted list) of
--- all function names that are defined by overlapping rules or rules
--- containing free variables which might be called.
--- In addition, the second component is (possibly) the list of
--- functions from which this non-deterministic function is called.
--- The length of this list is limited by 'maxDepsLength' in the
--- `NonDetAllDeps` analysis or to 1 (i.e., only the direct caller is
--- stored) in the `NonDetDeps` analysis.
type NonDetDeps = [(QName,[QName])]
--- The maximal length of a call chain associated with a non-deterministic
--- operation dependency. We limit the length in order to avoid large
--- analysis times for the `NonDetAllDeps` analysis.
maxDepsLength :: Int
maxDepsLength = 10
-- Show determinism dependency information as a string.
showNonDetDeps :: AOutFormat -> NonDetDeps -> String
showNonDetDeps AText [] = "deterministic"
showNonDetDeps ANote [] = ""
showNonDetDeps ANote xs@(_:_) = intercalate " " (nub (map (snd . fst) xs))
showNonDetDeps AText xs@(_:_) =
"depends on non-det. operations: " ++
intercalate ", " (map showNDOpInfo xs)
where
showNDOpInfo (ndop,cfs) = showQName ndop ++
(if null cfs
then ""
else " (called from " ++ intercalate " -> " (map showQName cfs) ++ ")")
showQName (mn,fn) = mn++"."++fn
--- Non-deterministic dependency analysis.
--- The analysis computes for each operation the set of operations
--- with a non-deterministic definition which might be called by this
--- operation. Non-deterministic operations that are called by other
--- non-deterministic operations are ignored so that only the first
--- (w.r.t. the call sequence) non-deterministic operations are returned.
nondetDepAnalysis :: Analysis NonDetDeps
nondetDepAnalysis =
dependencyFuncAnalysis "NonDetDeps" [] (nondetDeps False)
--- Non-deterministic dependency analysis.
--- The analysis computes for each operation the set of *all* operations
--- with a non-deterministic definition which might be called by this
--- operation.
nondetDepAllAnalysis :: Analysis NonDetDeps
nondetDepAllAnalysis =
dependencyFuncAnalysis "NonDetAllDeps" [] (nondetDeps True)
-- An operation is non-deterministic if its definition is potentially
-- non-deterministic (i.e., the dependency is the operation itself)
-- or it depends on some called non-deterministic function.
nondetDeps :: Bool -> FuncDecl -> [(QName,NonDetDeps)] -> NonDetDeps
nondetDeps alldeps func@(Func f _ _ _ rule) calledFuncs =
let calledndfuncs = sort (nub (map addCaller (calledNDFuncsInRule rule)))
addCaller (ndf,cfs)
| null cfs = (ndf,[f])
| alldeps && f `notElem` cfs
&& length cfs < maxDepsLength = (ndf,f:cfs)
| otherwise = (ndf,cfs)
in if isNondetDefined func
then (f,[]) : (if alldeps then calledndfuncs else [])
else calledndfuncs
where
calledNDFuncsInRule (Rule _ e) = calledNDFuncs e
calledNDFuncsInRule (External _) = []
calledNDFuncs (Var _) = []
calledNDFuncs (Lit _) = []
calledNDFuncs (Free _ e) = calledNDFuncs e
calledNDFuncs (Let bs e) =
concatMap calledNDFuncs (map snd bs) ++ calledNDFuncs e
calledNDFuncs (Or e1 e2) = calledNDFuncs e1 ++ calledNDFuncs e2
calledNDFuncs (Case _ e bs) =
calledNDFuncs e ++ concatMap (\ (Branch _ be) -> calledNDFuncs be) bs
calledNDFuncs (Typed e _) = calledNDFuncs e
calledNDFuncs (Comb _ qf@(mn,fn) es)
| mn == "SetFunctions" && take 3 fn == "set" && all isDigit (drop 3 fn)
= -- non-determinism of function (first argument) is encapsulated so that
-- its called ND functions are not relevant:
if null es then [] -- this case should not occur
else concatMap calledNDFuncs (tail es)
| isStrongEncapsOp qf
= -- non-determinism of argument is encapsulated so that
-- its called ND functions are not relevant:
[]
| otherwise
= maybe [] id (lookup qf calledFuncs) ++ concatMap calledNDFuncs es
------------------------------------------------------------------------------
pre :: String -> QName
pre n = ("Prelude",n)
------------------------------------------------------------------------------