module Test.Inspection.Plugin (plugin) where
import Control.Monad
import System.Exit
import Data.Either
import Data.Maybe
import Data.List
import Data.Bifunctor
import qualified Language.Haskell.TH.Syntax as TH
import GhcPlugins hiding (SrcLoc)
import Test.Inspection.Internal (KeepAlive(..))
import Test.Inspection (Obligation(..), Property(..))
import Test.Inspection.Core
plugin :: Plugin
plugin = defaultPlugin { installCoreToDos = install }
data UponFailure = AbortCompilation | KeepGoing deriving Eq
install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install args passes = return $ passes ++ [pass]
where
pass = CoreDoPluginPass "Test.Inspection" (proofPass upon_failure)
upon_failure | "keep-going" `elem` args = KeepGoing
| otherwise = AbortCompilation
extractObligations :: ModGuts -> (ModGuts, [Obligation])
extractObligations guts = (guts { mg_rules = rules', mg_anns = anns_clean }, obligations)
where
rules' = mg_rules guts
(anns', obligations) = partitionMaybe findObligationAnn (mg_anns guts)
anns_clean = filter (not . isKeepAliveAnn) anns'
isKeepAliveAnn :: Annotation -> Bool
isKeepAliveAnn (Annotation (NamedTarget _) payload)
| Just KeepAlive <- fromSerialized deserializeWithData payload
= True
isKeepAliveAnn _
= False
findObligationAnn :: Annotation -> Maybe Obligation
findObligationAnn (Annotation (ModuleTarget _) payload)
| Just obl <- fromSerialized deserializeWithData payload
= Just obl
findObligationAnn _
= Nothing
prettyObligation :: Module -> Obligation -> String
prettyObligation mod (Obligation {..}) =
maybe "" myPrettySrcLoc srcLoc ++ ": " ++
"inspecting " ++ prettyProperty mod target property ++
(if expectFail then " (failure expected)" else "")
prettyProperty :: Module -> TH.Name -> Property -> String
prettyProperty mod target (EqualTo n2) = showTHName mod target ++ " === " ++ showTHName mod n2
prettyProperty mod target (NoType t) = showTHName mod target ++ " `hasNoType` " ++ showTHName mod t
prettyProperty mod target NoAllocation = showTHName mod target ++ " does not allocate"
showTHName :: Module -> TH.Name -> String
showTHName mod (TH.Name occ (TH.NameQ m))
| moduleNameString (moduleName mod) == TH.modString m = TH.occString occ
showTHName mod (TH.Name occ (TH.NameG _ _ m))
| moduleNameString (moduleName mod) == TH.modString m = TH.occString occ
showTHName _ n = show n
checkObligation :: ModGuts -> Obligation -> CoreM Bool
checkObligation guts obl = do
putMsgS $ prettyObligation (mg_module guts) obl
res <- checkProperty guts (target obl) (property obl)
case (res, expectFail obl) of
(Nothing, False) -> do
return True
(Nothing, True) -> do
putMsgS "Obligation passes unexpectedly"
return False
(Just reportMsg, False) -> do
putMsgS "Obligation fails"
reportMsg
return False
(Just _, True) -> do
return True
type Result = Maybe (CoreM ())
lookupNameInGuts :: ModGuts -> Name -> Maybe (Var, CoreExpr)
lookupNameInGuts guts n = listToMaybe
[ (v,e)
| (v,e) <- flattenBinds (mg_binds guts)
, getName v == n
]
checkProperty :: ModGuts -> TH.Name -> Property -> CoreM Result
checkProperty guts thn1 (EqualTo thn2) = do
Just n1 <- thNameToGhcName thn1
Just n2 <- thNameToGhcName thn2
let p1 = lookupNameInGuts guts n1
let p2 = lookupNameInGuts guts n2
if | n1 == n2
-> return Nothing
| Just (_, Var other) <- p1, getName other == n2
-> return Nothing
| Just (_, Var other) <- p2, getName other == n1
-> return Nothing
| Just (v1, _) <- p1
, Just (v2, _) <- p2
, eqSlice (slice binds v1) (slice binds v2)
-> return Nothing
| Just (_, e1) <- p1
, Just (_, e2) <- p2
-> pure . Just $ do
putMsg $
nest 4 (hang (text "LHS" <> colon) 4 (ppr e1)) $$
nest 4 (hang (text "RHS" <> colon) 4 (ppr e2))
| Nothing <- p1
, Nothing <- p2
-> pure . Just $ do
putMsg $ ppr n1 <+> text " and " <+> ppr n2 <+>
text "are different external names"
where
binds = flattenBinds (mg_binds guts)
checkProperty guts thn (NoType tht) = do
Just n <- thNameToGhcName thn
Just t <- thNameToGhcName tht
case lookupNameInGuts guts n of
Nothing -> pure . Just $ do
putMsg $ ppr n <+> text "is not a local name"
Just (v, _) -> case freeOfType (slice binds v) t of
Just (v',e') -> pure . Just $ putMsg $ nest 4 (ppr v' <+> text "=" <+> ppr e')
Nothing -> pure Nothing
where binds = flattenBinds (mg_binds guts)
checkProperty guts thn NoAllocation = do
Just n <- thNameToGhcName thn
case lookupNameInGuts guts n of
Nothing -> pure . Just $ do
putMsg $ ppr n <+> text "is not a local name"
Just (v, _) -> case doesNotAllocate (slice binds v) of
Just (v',e') -> pure . Just $ putMsg $ nest 4 (ppr v' <+> text "=" <+> ppr e')
Nothing -> pure Nothing
where binds = flattenBinds (mg_binds guts)
proofPass :: UponFailure -> ModGuts -> CoreM ModGuts
proofPass upon_failure guts = do
dflags <- getDynFlags
when (optLevel dflags < 1) $
warnMsg $ fsep $ map text $ words "Test.Inspection: Compilation without -O detected. Expect optimizations to fail."
let (guts', obligations) = extractObligations guts
ok <- and <$> mapM (checkObligation guts') obligations
if ok
then do
let (_m,n) = bimap length length $ partition expectFail obligations
putMsg $ text "Test.Inspection tested" <+> ppr n <+>
text "obligation" <> (if n == 1 then empty else text "s")
else do
case upon_failure of
AbortCompilation -> do
errorMsg $ text "inspection testing unsuccessful"
liftIO $ exitFailure
KeepGoing -> do
warnMsg $ text "inspection testing unsuccessful"
return guts'
partitionMaybe :: (a -> Maybe b) -> [a] -> ([a], [b])
partitionMaybe f = partitionEithers . map (\x -> maybe (Left x) Right (f x))
myPrettySrcLoc :: TH.Loc -> String
myPrettySrcLoc TH.Loc {..}
= foldr (++) ""
[ loc_filename, ":"
, show (fst loc_start), ":"
, show (snd loc_start)
]