liquidhaskell-boot-0.9.6.3: src/Language/Haskell/Liquid/GHC/Plugin/SpecFinder.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE RankNTypes #-}
module Language.Haskell.Liquid.GHC.Plugin.SpecFinder
( findRelevantSpecs
, findCompanionSpec
, SpecFinderResult(..)
, SearchLocation(..)
, configToRedundantDependencies
) where
import qualified Language.Haskell.Liquid.GHC.Plugin.Util as Util
import Language.Haskell.Liquid.GHC.Plugin.Types
import Language.Haskell.Liquid.Types.Types
import Language.Haskell.Liquid.Types.Specs hiding (Spec)
import qualified Language.Haskell.Liquid.Misc as Misc
import Language.Haskell.Liquid.Parse ( specSpecificationP )
import Language.Fixpoint.Utils.Files ( Ext(Spec), withExt )
import qualified Liquid.GHC.API as O
import Liquid.GHC.API as GHC
import Data.Bifunctor
import qualified Data.Char
import Data.IORef
import Data.Maybe
import Control.Exception
import Control.Monad ( foldM )
import Control.Monad.Trans ( lift )
import Control.Monad.Trans.Maybe
import Text.Megaparsec.Error
type SpecFinder m = Module -> MaybeT IO SpecFinderResult
-- | The result of searching for a spec.
data SpecFinderResult =
SpecNotFound Module
| SpecFound Module SearchLocation BareSpec
| LibFound Module SearchLocation LiquidLib
data SearchLocation =
InterfaceLocation
-- ^ The spec was loaded from the annotations of an interface.
| DiskLocation
-- ^ The spec was loaded from disk (e.g. 'Prelude.spec' or similar)
deriving Show
-- | Load any relevant spec for the input list of 'Module's, by querying both the 'ExternalPackageState'
-- and the 'HomePackageTable'.
--
-- Specs come from the interface files of the given modules or their matching
-- _LHAssumptions modules. A module @M@ only matches with a module named
-- @M_LHAssumptions@.
--
-- Assumptions are taken from _LHAssumptions modules only if the interface
-- file of the matching module contains no spec.
findRelevantSpecs :: [String] -- ^ Package to exclude for loading LHAssumptions
-> HscEnv
-> [Module]
-- ^ Any relevant module fetched during dependency-discovery.
-> TcM [SpecFinderResult]
findRelevantSpecs lhAssmPkgExcludes hscEnv mods = do
eps <- liftIO $ readIORef (euc_eps $ ue_eps $ hsc_unit_env hscEnv)
foldM (loadRelevantSpec eps) mempty mods
where
loadRelevantSpec :: ExternalPackageState -> [SpecFinderResult] -> Module -> TcM [SpecFinderResult]
loadRelevantSpec eps !acc currentModule = do
res <- liftIO $ runMaybeT $
lookupInterfaceAnnotations eps (ue_hpt $ hsc_unit_env hscEnv) currentModule
case res of
Nothing -> do
mAssm <- loadModuleLHAssumptionsIfAny currentModule
return $ fromMaybe (SpecNotFound currentModule) mAssm : acc
Just specResult ->
return (specResult : acc)
loadModuleLHAssumptionsIfAny m | isImportExcluded m = return Nothing
| otherwise = do
let assumptionsModName = assumptionsModuleName m
-- loadInterface might mutate the EPS if the module is
-- not already loaded
res <- liftIO $ findImportedModule hscEnv assumptionsModName NoPkgQual
case res of
Found _ assumptionsMod -> do
_ <- initIfaceTcRn $ loadInterface "liquidhaskell assumptions" assumptionsMod ImportBySystem
-- read the EPS again
eps2 <- liftIO $ readIORef (euc_eps $ ue_eps $ hsc_unit_env hscEnv)
-- now look up the assumptions
liftIO $ runMaybeT $ lookupInterfaceAnnotationsEPS eps2 assumptionsMod
FoundMultiple{} -> failWithTc $ mkTcRnUnknownMessage $ mkPlainError [] $
cannotFindModule hscEnv assumptionsModName res
_ -> return Nothing
isImportExcluded m =
let s = takeWhile Data.Char.isAlphaNum $ unitString (moduleUnit m)
in elem s lhAssmPkgExcludes
assumptionsModuleName m =
mkModuleNameFS $ moduleNameFS (moduleName m) <> "_LHAssumptions"
-- | If this module has a \"companion\" '.spec' file sitting next to it, this 'SpecFinder'
-- will try loading it.
findCompanionSpec :: HscEnv -> Module -> IO SpecFinderResult
findCompanionSpec hscEnv m = do
res <- runMaybeT $ lookupCompanionSpec hscEnv m
case res of
Nothing -> pure $ SpecNotFound m
Just s -> pure s
-- | Load a spec by trying to parse the relevant \".spec\" file from the filesystem.
lookupInterfaceAnnotations :: ExternalPackageState -> HomePackageTable -> SpecFinder m
lookupInterfaceAnnotations eps hpt thisModule = do
lib <- MaybeT $ pure $ Util.deserialiseLiquidLib thisModule eps hpt
pure $ LibFound thisModule InterfaceLocation lib
lookupInterfaceAnnotationsEPS :: ExternalPackageState -> SpecFinder m
lookupInterfaceAnnotationsEPS eps thisModule = do
lib <- MaybeT $ pure $ Util.deserialiseLiquidLibFromEPS thisModule eps
pure $ LibFound thisModule InterfaceLocation lib
-- | If this module has a \"companion\" '.spec' file sitting next to it, this 'SpecFinder'
-- will try loading it.
lookupCompanionSpec :: HscEnv -> SpecFinder m
lookupCompanionSpec hscEnv thisModule = do
modSummary <- MaybeT $ pure $ lookupModSummary hscEnv (moduleName thisModule)
file <- MaybeT $ pure (ml_hs_file . ms_location $ modSummary)
parsed <- MaybeT $ do
mbSpecContent <- try (Misc.sayReadFile (specFile file))
case mbSpecContent of
Left (_e :: SomeException) -> pure Nothing
Right raw -> pure $ Just $ specSpecificationP (specFile file) raw
case parsed of
Left peb -> do
let errMsg = O.text "Error when parsing "
O.<+> O.text (specFile file) O.<+> O.text ":"
O.<+> O.text (errorBundlePretty peb)
lift $ Util.pluginAbort (O.showSDoc (hsc_dflags hscEnv) errMsg)
Right (_, spec) -> do
let bareSpec = toBareSpec spec
pure $ SpecFound thisModule DiskLocation bareSpec
where
specFile :: FilePath -> FilePath
specFile fp = withExt fp Spec
-- | Returns a list of 'StableModule's which can be filtered out of the dependency list, because they are
-- selectively \"toggled\" on and off by the LiquidHaskell's configuration, which granularity can be
-- /per module/.
configToRedundantDependencies :: HscEnv -> Config -> IO [StableModule]
configToRedundantDependencies env cfg = do
catMaybes <$> mapM (lookupModule' . first ($ cfg)) configSensitiveDependencies
where
lookupModule' :: (Bool, ModuleName) -> IO (Maybe StableModule)
lookupModule' (fetchModule, modName)
| fetchModule = lookupLiquidBaseModule modName
| otherwise = pure Nothing
lookupLiquidBaseModule :: ModuleName -> IO (Maybe StableModule)
lookupLiquidBaseModule mn = do
res <- findImportedModule env mn (renamePkgQual (hsc_unit_env env) mn (Just "liquidhaskell"))
case res of
Found _ mdl -> pure $ Just (toStableModule mdl)
_ -> pure Nothing
-- | Static associative map of the 'ModuleName' that needs to be filtered from the final 'TargetDependencies'
-- due to some particular configuration options.
--
-- Modify this map to add any extra special case. Remember that the semantic is not which module will be
-- /added/, but rather which one will be /removed/ from the final list of dependencies.
--
configSensitiveDependencies :: [(Config -> Bool, ModuleName)]
configSensitiveDependencies = [
(not . totalityCheck, mkModuleName "Liquid.Prelude.Totality_LHAssumptions")
, (linear, mkModuleName "Liquid.Prelude.Real_LHAssumptions")
]