{-# LANGUAGE PatternGuards #-}
module Idris.Coverage where
import Core.TT
import Core.Evaluate
import Core.CaseTree
import Idris.AbsSyntax
import Idris.Delaborate
import Idris.Error
import Data.List
import Data.Either
import Data.Maybe
import Debug.Trace
import Control.Monad.State
mkPatTm :: PTerm -> Idris Term
mkPatTm t = do i <- getIState
let timp = addImpl' True [] [] i t
evalStateT (toTT timp) 0
where
toTT (PRef _ n) = do i <- lift getIState
case lookupDef n (tt_ctxt i) of
[TyDecl nt _] -> return $ P nt n Erased
_ -> return $ P Ref n Erased
toTT (PApp _ t args) = do t' <- toTT t
args' <- mapM (toTT . getTm) args
return $ mkApp t' args'
toTT _ = do v <- get
put (v + 1)
return (P Bound (MN v "imp") Erased)
-- Given a list of LHSs, generate a extra clauses which cover the remaining
-- cases. The ones which haven't been provided are marked 'absurd' so that the
-- checker will make sure they can't happen.
-- This will only work after the given clauses have been typechecked and the
-- names are fully explicit!
genClauses :: FC -> Name -> [Term] -> [PClause] -> Idris [PTerm]
genClauses fc n xs given
= do i <- getIState
let lhss = map (getLHS i) xs
let argss = transpose lhss
let all_args = map (genAll i) argss
logLvl 5 $ "COVERAGE of " ++ show n
logLvl 5 $ show (map length argss) ++ "\n" ++ show (map length all_args)
logLvl 10 $ show argss ++ "\n" ++ show all_args
logLvl 10 $ "Original: \n" ++
showSep "\n" (map (\t -> showImp True (delab' i t True)) xs)
-- add an infinite supply of explicit arguments to update the possible
-- cases for (the return type may be variadic, or function type, sp
-- there may be more case splitting that the idris_implicits record
-- suggests)
let parg = case lookupCtxt n (idris_implicits i) of
(p : _) -> p ++ repeat (PExp 0 False Placeholder "")
_ -> repeat (pexp Placeholder)
let tryclauses = mkClauses parg all_args
logLvl 2 $ show (length tryclauses) ++ " initially to check"
let new = filter (noMatch i) (mnub i tryclauses)
logLvl 1 $ show (length new) ++ " clauses to check for impossibility"
logLvl 5 $ "New clauses: \n" ++ showSep "\n" (map (showImp True) new)
-- ++ " from:\n" ++ showSep "\n" (map (showImp True) tryclauses)
return new
-- return (map (\t -> PClause n t [] PImpossible []) new)
where getLHS i term
| (f, args) <- unApply term = map (\t -> delab' i t True) args
| otherwise = []
lhsApp (PClause _ _ l _ _ _) = l
lhsApp (PWith _ _ l _ _ _) = l
mnub i [] = []
mnub i (x : xs) =
let xs' = filter (\t -> case matchClause i x t of
Right _ -> False
Left _ -> True) xs in
x : mnub i xs'
-- if (any (\t -> case matchClause i x t of
-- Right _ -> True
-- Left _ -> False) xs) then mnub i xs
-- else x : mnub i xs
noMatch i tm = all (\x -> case matchClause i (delab' i x True) tm of
Right _ -> False
Left miss -> True) xs
mkClauses :: [PArg] -> [[PTerm]] -> [PTerm]
mkClauses parg args
| all (== [Placeholder]) args = []
mkClauses parg args
= do args' <- mkArg args
let tm = PApp fc (PRef fc n) (zipWith upd args' parg)
return tm
where
mkArg :: [[PTerm]] -> [[PTerm]]
mkArg [] = return []
mkArg (a : as) = do a' <- a
as' <- mkArg as
return (a':as')
fnub xs = fnub' [] xs
fnub' acc (x : xs) | x `qelem` acc = fnub' acc (filter (not.(quickEq x)) xs)
| otherwise = fnub' (x : acc) xs
fnub' acc [] = acc
-- quick check for constructor equality
quickEq :: PTerm -> PTerm -> Bool
quickEq (PRef _ n) (PRef _ n') = n == n'
quickEq (PApp _ t as) (PApp _ t' as')
| length as == length as'
= quickEq t t' && and (zipWith quickEq (map getTm as) (map getTm as'))
quickEq Placeholder Placeholder = True
quickEq _ _ = False
qelem x [] = False
qelem x (y : ys) | x `quickEq` y = True
| otherwise = qelem x ys
-- FIXME: Just look for which one is the deepest, then generate all
-- possibilities up to that depth.
genAll :: IState -> [PTerm] -> [PTerm]
genAll i args
= case filter (/=Placeholder) $ fnub (concatMap otherPats (fnub args)) of
[] -> [Placeholder]
xs -> xs
where
conForm (PApp _ (PRef fc n) _) = isConName n (tt_ctxt i)
conForm (PRef fc n) = isConName n (tt_ctxt i)
conForm _ = False
nubMap f acc [] = acc
nubMap f acc (x : xs) = nubMap f (fnub' acc (f x)) xs
otherPats :: PTerm -> [PTerm]
otherPats o@(PRef fc n) = ops fc n [] o
otherPats o@(PApp _ (PRef fc n) xs) = ops fc n xs o
otherPats arg = return Placeholder
ops fc n xs_in o
| (TyDecl c@(DCon _ arity) ty : _) <- lookupDef n (tt_ctxt i)
= do let force = getForceable i n -- no need to generate forceable positions
let xs = dropForce force xs_in 0
xs' <- mapM otherPats (map getTm xs)
let p = PApp fc (PRef fc n) (zipWith upd xs' xs)
let tyn = getTy n (tt_ctxt i)
case lookupCtxt tyn (idris_datatypes i) of
(TI ns _ _ : _) -> p : map (mkPat fc) (ns \\ [n])
_ -> [p]
ops fc n arg o = return Placeholder
getForceable i n = case lookupCtxt n (idris_optimisation i) of
[Optimise _ fs _] -> fs
_ -> []
dropForce force (x : xs) i | i `elem` force
= upd Placeholder x : dropForce force xs (i + 1)
dropForce force (x : xs) i = x : dropForce force xs (i + 1)
dropForce _ [] _ = []
getTy n ctxt = case lookupTy n ctxt of
(t : _) -> case unApply (getRetTy t) of
(P _ tyn _, _) -> tyn
x -> error $ "Can't happen getTy 1 " ++ show (n, x)
_ -> error "Can't happen getTy 2"
mkPat fc x = case lookupCtxt x (idris_implicits i) of
(pargs : _)
-> PApp fc (PRef fc x) (map (upd Placeholder) pargs)
_ -> error "Can't happen - genAll"
upd p' p = p { getTm = p' }
-- Check if, in a given type n, the constructor cn : ty is strictly positive,
-- and update the context accordingly
checkPositive :: Name -> (Name, Type) -> Idris ()
checkPositive n (cn, ty)
= do let p = cp ty
i <- getIState
let tot = if p then Total (args ty) else Partial NotPositive
let ctxt' = setTotal cn tot (tt_ctxt i)
putIState (i { tt_ctxt = ctxt' })
logLvl 5 $ "Constructor " ++ show cn ++ " is " ++ show tot
addIBC (IBCTotal cn tot)
where
args t = [0..length (getArgTys t)-1]
cp (Bind n (Pi aty) sc) = posArg aty && cp sc
cp t = True
posArg (Bind _ (Pi nty) sc)
| (P _ n' _, args) <- unApply nty
= n /= n' && posArg sc
posArg t = True
calcProd :: IState -> FC -> Name -> [([Name], Term, Term)] -> Idris Totality
calcProd i fc topn pats
= cp topn pats []
where
-- every application of n must be in an argument of a coinductive
-- constructor, in every function reachable from here in the
-- call graph.
cp n pats done = do patsprod <- mapM (prodRec n done) pats
if (and patsprod)
then return Productive
else return (Partial NotProductive)
prodRec :: Name -> [Name] -> ([Name], Term, Term) -> Idris Bool
prodRec n done _ | n `elem` done = return True
prodRec n done (_, _, tm) = prod n done False tm
prod :: Name -> [Name] -> Bool -> Term -> Idris Bool
prod n done ok ap@(App _ _)
| (P _ (UN "lazy") _, [_, arg]) <- unApply ap = prod n done ok arg
| (P nt f _, args) <- unApply ap
= do recOK <- checkProdRec (n:done) f
let ctxt = tt_ctxt i
let [ty] = lookupTy f ctxt -- must exist!
let co = cotype nt f ty in
if (not recOK) then return False else
if f == topn
then do argsprod <- mapM (prod n done co) args
return (and (ok : argsprod) )
else do argsprod <- mapM (prod n done co) args
return (and argsprod)
prod n done ok (App f a) = liftM2 (&&) (prod n done False f)
(prod n done False a)
prod n done ok (Bind _ (Let t v) sc)
= liftM2 (&&) (prod n done False v) (prod n done False v)
prod n done ok (Bind _ b sc) = prod n done ok sc
prod n done ok t = return True
checkProdRec :: [Name] -> Name -> Idris Bool
checkProdRec done f
= case lookupCtxt f (idris_patdefs i) of
[(def, _)] -> do ok <- mapM (prodRec f done) def
return (and ok)
_ -> return True -- defined elsewhere, can't call topn
cotype (DCon _ _) n ty
| (P _ t _, _) <- unApply (getRetTy ty)
= case lookupCtxt t (idris_datatypes i) of
[TI _ True _] -> True
_ -> False
cotype nt n ty = False
calcTotality :: [Name] -> FC -> Name -> [([Name], Term, Term)]
-> Idris Totality
calcTotality path fc n pats
= do i <- getIState
let opts = case lookupCtxt n (idris_flags i) of
[fs] -> fs
_ -> []
case mapMaybe (checkLHS i) (map (\ (_, l, r) -> l) pats) of
(failure : _) -> return failure
_ -> if (Coinductive `elem` opts)
then calcProd i fc n pats
else checkSizeChange n
where
checkLHS i (P _ fn _)
= case lookupTotal fn (tt_ctxt i) of
[Partial _] -> return (Partial (Other [fn]))
_ -> Nothing
checkLHS i (App f a) = mplus (checkLHS i f) (checkLHS i a)
checkLHS _ _ = Nothing
checkTotality :: [Name] -> FC -> Name -> Idris Totality
checkTotality path fc n
| n `elem` path = return (Partial (Mutual (n : path)))
| otherwise = do
t <- getTotality n
updateContext (simplifyCasedef n)
ctxt <- getContext
i <- getIState
let opts = case lookupCtxt n (idris_flags i) of
[fs] -> fs
_ -> []
t' <- case t of
Unchecked ->
case lookupDef n ctxt of
[CaseOp _ _ _ _ pats _ _ _ _] ->
do t' <- if AssertTotal `elem` opts
then return $ Total []
else calcTotality path fc n pats
setTotality n t'
addIBC (IBCTotal n t')
-- if it's not total, it can't reduce, to keep
-- typechecking decidable
case t' of
-- FIXME: Put this back when we can handle mutually recursive things
-- p@(Partial _) ->
-- do setAccessibility n Frozen
-- addIBC (IBCAccess n Frozen)
-- logLvl 5 $ "HIDDEN: "
-- ++ show n ++ show p
_ -> return ()
return t'
_ -> return $ Total []
x -> return x
case t' of
Total _ -> return t'
Productive -> return t'
e -> do w <- cmdOptType WarnPartial
if TotalFn `elem` opts
then totalityError t'
else do when (w && not (PartialFn `elem` opts)) $
warnPartial n t'
return t'
where
totalityError t = tclift $ tfail (At fc (Msg (show n ++ " is " ++ show t)))
warnPartial n t
= do i <- getIState
case lookupDef n (tt_ctxt i) of
[x] -> do
iputStrLn $ show fc ++ ":Warning - " ++ show n ++ " is " ++ show t
-- ++ "\n" ++ show x
-- let cg = lookupCtxtName Nothing n (idris_callgraph i)
-- iputStrLn (show cg)
checkDeclTotality :: (FC, Name) -> Idris Totality
checkDeclTotality (fc, n)
= do logLvl 2 $ "Checking " ++ show n ++ " for totality"
-- buildSCG (fc, n)
-- logLvl 2 $ "Built SCG"
checkTotality [] fc n
-- Calculate the size change graph for this definition
-- SCG for a function f consists of a list of:
-- (g, [(a1, sizechange1), (a2, sizechange2), ..., (an, sizechangen)])
-- where g is a function called
-- a1 ... an are the arguments of f in positions 1..n of g
-- sizechange1 ... sizechange2 is how their size has changed wrt the input
-- to f
-- Nothing, if the argument is unrelated to the input
buildSCG :: (FC, Name) -> Idris ()
buildSCG (_, n) = do
ist <- getIState
case lookupCtxt n (idris_callgraph ist) of
[cg] -> case lookupDef n (tt_ctxt ist) of
[CaseOp _ _ _ pats _ args sc _ _] ->
do logLvl 2 $ "Building SCG for " ++ show n ++ " from\n"
++ show pats ++ "\n" ++ show sc
let newscg = buildSCG' ist (rights pats) args
logLvl 5 $ show newscg
addToCG n ( cg { scg = newscg } )
[] -> logLvl 5 $ "Could not build SCG for " ++ show n ++ "\n"
x -> error $ "buildSCG: " ++ show (n, x)
buildSCG' :: IState -> [(Term, Term)] -> [Name] -> [SCGEntry]
buildSCG' ist pats args = nub $ concatMap scgPat pats where
scgPat (lhs, rhs) = let (f, pargs) = unApply (dePat lhs) in
findCalls (dePat rhs) (patvars lhs) pargs
findCalls ap@(App f a) pvs pargs
| (P _ (UN "lazy") _, [_, arg]) <- unApply ap
= findCalls arg pvs pargs
| (P _ n _, args) <- unApply ap
= mkChange n args pargs ++
concatMap (\x -> findCalls x pvs pargs) args
findCalls (App f a) pvs pargs
= findCalls f pvs pargs ++ findCalls a pvs pargs
findCalls (Bind n (Let t v) e) pvs pargs
= findCalls v pvs pargs ++ findCalls e (n : pvs) pargs
findCalls (Bind n _ e) pvs pargs
= findCalls e (n : pvs) pargs
findCalls (P _ f _ ) pvs pargs
| not (f `elem` pvs) = [(f, [])]
findCalls _ _ _ = []
mkChange n args pargs = [(n, sizes args)]
where
sizes [] = []
sizes (a : as) = checkSize a pargs 0 : sizes as
-- find which argument in pargs is smaller than, if any
checkSize a (p : ps) i
| a == p = Just (i, Same)
| smaller Nothing a (p, Nothing) = Just (i, Smaller)
| otherwise = checkSize a ps (i + 1)
checkSize a [] i = Nothing
-- the smaller thing we find must be the same type as , and
-- not be coinductive - so carry the type of the constructor we've
-- gone under.
smaller (Just tyn) a (t, Just tyt)
| a == t = isInductive (fst (unApply (getRetTy tyn)))
(fst (unApply (getRetTy tyt)))
smaller ty a (ap@(App f s), _)
| (P (DCon _ _) n _, args) <- unApply ap
= let tyn = getType n in
any (smaller (ty `mplus` Just tyn) a)
(zip args (map toJust (getArgTys tyn)))
-- check higher order recursive arguments
smaller ty (App f s) a = smaller ty f a
smaller _ _ _ = False
toJust (n, t) = Just t
getType n = case lookupTy n (tt_ctxt ist) of
[ty] -> ty -- must exist
isInductive (P _ nty _) (P _ nty' _) =
let co = case lookupCtxt nty (idris_datatypes ist) of
[TI _ x _] -> x
_ -> False in
nty == nty' && not co
isInductive _ _ = False
-- getTypeFam t | (P _ n _, _) <- unApply t
dePat (Bind x (PVar ty) sc) = dePat (instantiate (P Bound x ty) sc)
dePat t = t
patvars (Bind x (PVar _) sc) = x : patvars sc
patvars _ = []
{-
buildSCG' :: IState -> SC -> [Name] -> [SCGEntry]
buildSCG' ist sc args = -- trace ("Building SCG for " ++ show sc) $
nub $ scg sc (zip args args)
(zip args (zip args (repeat Same)))
where
scg :: SC -> [(Name, Name)] -> -- local var, originating top level var
[(Name, (Name, SizeChange))] -> -- orig to new, and relationship
[SCGEntry]
scg (Case x alts) vars szs
= let x' = findTL x vars in
concatMap (scgAlt x' vars szs) alts
where
findTL x vars
| Just x' <- lookup x vars
= if x' `elem` args then x'
else findTL x' vars
| otherwise = x
scg (STerm tm) vars szs = scgTerm tm vars szs
scg _ _ _ = []
-- how the arguments relate - either Smaller or Unknown
argRels :: Name -> [(Name, SizeChange)]
argRels n = let ctxt = tt_ctxt ist
[ty] = lookupTy n ctxt -- must exist!
P _ nty _ = fst (unApply (getRetTy ty))
co = case lookupCtxt nty (idris_datatypes ist) of
[TI _ x _] -> x
_ -> False
args = map snd (getArgTys ty) in
map (getRel co nty) (map (fst . unApply . getRetTy) args)
where
getRel True _ _ = (n, Unknown) -- coinductive
getRel _ ty (P _ n' _) | n' == ty = (n, Smaller)
getRel _ ty t = (n, Unknown)
scgAlt x vars szs (ConCase n _ args sc)
-- all args smaller than top variable of x in sc
-- (as long as they are in the same type family, and it's
-- not coinductive)
| Just tvar <- lookup x vars
= let arel = argRels n
szs' = zipWith (\arg (_,t) -> (arg, (x, t))) args arel
++ szs
vars' = nub (zip args (repeat tvar) ++ vars) in
scg sc vars' szs'
| otherwise = scg sc vars szs
scgAlt x vars szs (ConstCase _ sc) = scg sc vars szs
scgAlt x vars szs (DefaultCase sc) = scg sc vars szs
scgTerm f@(App _ _) vars szs
| (P _ (UN "lazy") _, [_, arg]) <- unApply f
= scgTerm arg vars szs
| (P _ fn _, args) <- unApply f
= let rest = concatMap (\x -> scgTerm x vars szs) args in
case lookup fn vars of
Just _ -> rest
Nothing -> nub $ (fn, map (mkChange szs) args) : rest
scgTerm (App f a) vars szs
= scgTerm f vars szs ++ scgTerm a vars szs
scgTerm (Bind n (Let t v) e) vars szs
= scgTerm v vars szs ++ scgTerm e vars szs
scgTerm (Bind n _ e) vars szs
= scgTerm e (nub ((n, n) : vars)) szs
scgTerm (P _ fn _) vars szs
= case lookup fn vars of
Just _ -> []
Nothing -> [(fn, [])]
scgTerm _ _ _ = []
mkChange :: [(Name, (Name, SizeChange))] -> Term
-> Maybe (Int, SizeChange)
mkChange szs tm
| (P _ (UN "lazy") _, [_, arg]) <- unApply tm = mkChange szs arg
| (P _ n ty, _) <- unApply tm -- get higher order args too
= do sc <- lookup n szs
case sc of
(_, Unknown) -> Nothing
(o, sc) -> do i <- getArgPos 0 o args
return (i, sc)
mkChange _ _ = Nothing
getArgPos :: Int -> Name -> [Name] -> Maybe Int
getArgPos i n [] = Nothing
getArgPos i n (x : xs) | n == x = Just i
| otherwise = getArgPos (i + 1) n xs
-}
checkSizeChange :: Name -> Idris Totality
checkSizeChange n = do
ist <- getIState
case lookupCtxt n (idris_callgraph ist) of
[cg] -> do let ms = mkMultiPaths ist [] (scg cg)
logLvl 5 ("Multipath for " ++ show n ++ ":\n" ++
"from " ++ show (scg cg) ++ "\n" ++
show (length ms) ++ "\n" ++
showSep "\n" (map show ms))
logLvl 6 (show cg)
-- every multipath must have an infinitely descending
-- thread, then the function terminates
-- also need to checks functions called are all total
-- (Unchecked is okay as we'll spot problems here)
let tot = map (checkMP ist (length (argsdef cg))) ms
logLvl 4 $ "Generated " ++ show (length tot) ++ " paths"
logLvl 6 $ "Paths for " ++ show n ++ " yield " ++ (show tot)
return (noPartial tot)
[] -> do logLvl 5 $ "No paths for " ++ show n
return Unchecked
type MultiPath = [SCGEntry]
mkMultiPaths :: IState -> MultiPath -> [SCGEntry] -> [MultiPath]
mkMultiPaths ist path [] = [reverse path]
mkMultiPaths ist path cg
= concat (map extend cg)
where extend (nextf, args)
| (nextf, args) `elem` path = [ reverse ((nextf, args) : path) ]
| [Unchecked] <- lookupTotal nextf (tt_ctxt ist)
= case lookupCtxt nextf (idris_callgraph ist) of
[ncg] -> mkMultiPaths ist ((nextf, args) : path) (scg ncg)
_ -> [ reverse ((nextf, args) : path) ]
| otherwise = [ reverse ((nextf, args) : path) ]
-- do (nextf, args) <- cg
-- if ((nextf, args) `elem` path)
-- then return (reverse ((nextf, args) : path))
-- else case lookupCtxt nextf (idris_callgraph ist) of
-- [ncg] -> mkMultiPaths ist ((nextf, args) : path) (scg ncg)
-- _ -> return (reverse ((nextf, args) : path))
-- If any route along the multipath leads to infinite descent, we're fine.
-- Try a route beginning with every argument.
-- If we reach a point we've been to before, but with a smaller value,
-- that means there is an infinitely descending path from that argument.
checkMP :: IState -> Int -> MultiPath -> Totality
checkMP ist i mp = if i > 0
then collapse (map (tryPath 0 [] mp) [0..i-1])
else tryPath 0 [] mp 0
where
tryPath' d path mp arg
= let res = tryPath d path mp arg in
trace (show mp ++ "\n" ++ show arg ++ " " ++ show res) res
tryPath :: Int -> [(SCGEntry, Int)] -> MultiPath -> Int -> Totality
tryPath desc path [] _ = Total []
-- tryPath desc path ((UN "believe_me", _) : _) arg
-- = Partial BelieveMe
-- if we get to a constructor, it's fine as long as it's strictly positive
tryPath desc path ((f, _) :es) arg
| [TyDecl (DCon _ _) _] <- lookupDef f (tt_ctxt ist)
= case lookupTotal f (tt_ctxt ist) of
[Total _] -> Unchecked -- okay so far
[Partial _] -> Partial (Other [f])
x -> error (show x)
| [TyDecl (TCon _ _) _] <- lookupDef f (tt_ctxt ist)
= Total []
tryPath desc path (e@(f, args) : es) arg
| e `elem` es && allNothing args = Partial (Mutual [f])
tryPath desc path (e@(f, nextargs) : es) arg
| Just d <- lookup e path
= if desc > 0
then -- trace ("Descent " ++ show (desc - d) ++ " "
-- ++ show (path, e)) $
Total []
else Partial (Mutual (map (fst . fst) path ++ [f]))
| [Unchecked] <- lookupTotal f (tt_ctxt ist) =
let argspos = collapseNothing (zip nextargs [0..]) in
collapse' Unchecked $
do (a, pos) <- argspos
case a of
Nothing -> -- don't know, but if the
-- rest definitely terminates without
-- any cycles with route so far,
-- then we might yet be total
case collapse (map (tryPath (-10000) ((e, 0):path) es)
[0..length nextargs - 1]) of
Total _ -> return Unchecked
x -> return x
Just (nextarg, sc) ->
if nextarg == arg then
case sc of
Same -> return $ tryPath desc ((e, desc) : path)
es pos
Smaller -> return $ tryPath (desc+1)
((e, desc):path)
es
pos
_ -> trace ("Shouldn't happen " ++ show e) $
return (Partial Itself)
else return Unchecked
| [Total a] <- lookupTotal f (tt_ctxt ist) = Total a
| [Partial _] <- lookupTotal f (tt_ctxt ist) = Partial (Other [f])
| otherwise = Unchecked
allNothing xs = null (collapseNothing (zip xs [0..]))
collapseNothing ((Nothing, _) : xs)
= filter (\ (x, _) -> case x of
Nothing -> False
_ -> True) xs
collapseNothing (x : xs) = x : collapseNothing xs
collapseNothing [] = []
noPartial (Partial p : xs) = Partial p
noPartial (_ : xs) = noPartial xs
noPartial [] = Total []
collapse xs = collapse' Unchecked xs
collapse' def (Total r : xs) = Total r
collapse' def (Unchecked : xs) = collapse' def xs
collapse' def (d : xs) = collapse' d xs
-- collapse' Unchecked [] = Total []
collapse' def [] = def