{-# LANGUAGE CPP #-} {-| This modules deals with how to find imported modules and loading their interface files. -} module Agda.Interaction.Imports where import Prelude hiding (catch) import Control.Monad.Error import Control.Monad.State import qualified Data.Map as Map import qualified Data.List as List import qualified Data.Set as Set import qualified Data.ByteString.Lazy as BS import Data.Generics import Data.List import Data.Map (Map) import System.Directory import System.Time import Control.OldException import qualified System.IO.UTF8 as UTF8 import System.FilePath (isAbsolute) import Agda.Syntax.Position import qualified Agda.Syntax.Concrete as C import Agda.Syntax.Abstract.Name import Agda.Syntax.Parser import Agda.Syntax.Scope.Base import Agda.Syntax.Scope.Monad import Agda.Syntax.Translation.ConcreteToAbstract import Agda.Syntax.Internal import Agda.Termination.TermCheck import Agda.TypeChecking.Reduce import Agda.TypeChecking.Monad import Agda.TypeChecking.Monad.Builtin import Agda.TypeChecking.Serialise import Agda.TypeChecking.Primitive import Agda.TypeChecker import Agda.Interaction.Options import Agda.Interaction.Highlighting.Precise (HighlightingInfo) import Agda.Interaction.Highlighting.Generate import Agda.Interaction.Highlighting.Emacs import Agda.Interaction.Highlighting.Vim import qualified Agda.Interaction.Highlighting.Range as R import Agda.Utils.FileName import Agda.Utils.Monad import Agda.Utils.IO import Agda.Utils.Impossible #include "../undefined.h" -- | Converts an Agda file name to the corresponding interface file -- name. toIFile :: FilePath -> FilePath toIFile = setExtension ".agdai" -- | Merge an interface into the current proof state. mergeInterface :: Interface -> TCM () mergeInterface i = do let sig = iSignature i builtin = Map.toList $ iBuiltin i prim = [ x | (_,Prim x) <- builtin ] bi = Map.fromList [ (x,Builtin t) | (x,Builtin t) <- builtin ] bs <- getBuiltinThings reportSLn "import.iface.merge" 10 $ "Merging interface" reportSLn "import.iface.merge" 20 $ " Current builtins " ++ show (Map.keys bs) ++ "\n" ++ " New builtins " ++ show (Map.keys bi) case Map.toList $ Map.intersection bs bi of [] -> return () (b, Builtin x):_ -> typeError $ DuplicateBuiltinBinding b x x (_, Prim{}):_ -> __IMPOSSIBLE__ modify $ \st -> st { stImports = unionSignatures [stImports st, sig] , stImportedBuiltins = stImportedBuiltins st `Map.union` bi } reportSLn "import.iface.merge" 20 $ " Rebinding primitives " ++ show prim prim <- Map.fromList <$> mapM rebind prim modify $ \st -> st { stImportedBuiltins = stImportedBuiltins st `Map.union` prim } where rebind x = do PrimImpl _ pf <- lookupPrimitiveFunction x return (x, Prim pf) addImportedThings :: Signature -> BuiltinThings PrimFun -> TCM () addImportedThings isig ibuiltin = modify $ \st -> st { stImports = unionSignatures [stImports st, isig] , stImportedBuiltins = Map.union (stImportedBuiltins st) ibuiltin } -- TODO: move data FileType = SourceFile | InterfaceFile findFile :: FileType -> ModuleName -> TCM FilePath findFile ft m = do let x = mnameToConcrete m dirs <- getIncludeDirs let files = [ dir ++ [slash] ++ file | dir <- dirs , file <- map (C.moduleNameToFileName x) exts ] files' <- liftIO $ filterM doesFileExist files files' <- liftIO $ nubFiles files' case files' of [] -> typeError $ FileNotFound m files file:_ -> return file where exts = case ft of SourceFile -> [".agda", ".lagda"] InterfaceFile -> [".agdai"] -- | Scope checks the given module. A proper version of the module -- name (with correct definition sites) is returned. scopeCheckImport :: ModuleName -> TCM (ModuleName, Map ModuleName Scope) scopeCheckImport x = do reportSLn "import.scope" 5 $ "Scope checking " ++ show x visited <- Map.keys <$> getVisitedModules reportSLn "import.scope" 10 $ " visited: " ++ show visited (i,t) <- getInterface x addImport x return (iModuleName i `withRangesOfQ` mnameToConcrete x, iScope i) alreadyVisited :: ModuleName -> TCM (Interface, ClockTime) -> TCM (Interface, ClockTime) alreadyVisited x getIface = do mm <- getVisitedModule x case mm of Just it -> do reportSLn "import.visit" 10 $ " Already visited " ++ show x return it Nothing -> do reportSLn "import.visit" 5 $ " Getting interface for " ++ show x (i, t) <- getIface reportSLn "import.visit" 5 $ " Now we've looked at " ++ show x visitModule x i t return (i, t) getInterface :: ModuleName -> TCM (Interface, ClockTime) getInterface x = alreadyVisited x $ addImportCycleCheck x $ do file <- findFile SourceFile x -- requires source to exist reportSLn "import.iface" 10 $ " Check for cycle" checkForImportCycle uptodate <- ifM ignoreInterfaces (return False) (liftIO $ toIFile file `isNewerThan` file) reportSLn "import.iface" 5 $ " " ++ show x ++ " is " ++ (if uptodate then "" else "not ") ++ "up-to-date." (i,t) <- if uptodate then skip x file else typeCheck file visited <- isVisited x reportSLn "import.iface" 5 $ if visited then " We've been here. Don't merge." else " New module. Let's check it out." unless visited $ mergeInterface i storeDecodedModule x i t return (i,t) where skip x file = do -- Examine the mtime of the interface file. If it is newer than the -- stored version (in stDecodedModules), or if there is no stored version, -- read and decode it. Otherwise use the stored version. let ifile = toIFile file t <- liftIO $ getModificationTime ifile mm <- getDecodedModule x mi <- case mm of Just (im, tm) -> if tm < t then do dropDecodedModule x reportSLn "import.iface" 5 $ " file is newer, re-reading " ++ ifile liftIO $ readInterface ifile else do reportSLn "import.iface" 5 $ " using stored version of " ++ ifile return (Just im) Nothing -> do reportSLn "import.iface" 5 $ " no stored version, reading " ++ ifile liftIO $ readInterface ifile -- Check that it's the right version case mi of Nothing -> do reportSLn "import.iface" 5 $ " bad interface, re-type checking" typeCheck file Just i -> do reportSLn "import.iface" 5 $ " imports: " ++ show (iImportedModules i) ts <- map snd <$> mapM getInterface (iImportedModules i) -- If any of the imports are newer we need to retype check if any (> t) ts then do -- liftIO close -- Close the interface file. See above. typeCheck file else do reportSLn "" 1 $ "Skipping " ++ show x ++ " (" ++ ifile ++ ")." return (i, t) typeCheck file = do -- Do the type checking ms <- getImportPath vs <- getVisitedModules ds <- getDecodedModules opts <- commandLineOptions trace <- getTrace isig <- getImportedSignature ibuiltin <- gets stImportedBuiltins r <- liftIO $ runTCM $ -- Every interface should be -- treated in isolation. createInterface opts trace ms vs ds isig ibuiltin (Just x) file False case r of Left err -> throwError err Right (_, Warnings termErrs@(_:_) []) -> do typeError $ TerminationCheckFailed termErrs Right (_, Warnings _ _) -> __IMPOSSIBLE__ Right (_, Success vs ds i isig ibuiltin) -> do -- writeInterface (used by createInterface) may -- remove ifile. let ifile = toIFile file t <- liftIO $ ifM (doesFileExist ifile) (getModificationTime ifile) getClockTime setVisitedModules vs setDecodedModules ds -- We need to add things imported when checking -- the imported modules. addImportedThings isig ibuiltin return (i, t) readInterface :: FilePath -> IO (Maybe Interface) readInterface file = do -- Decode the interface file (s, close) <- readBinaryFile' file do i <- decode s -- Force the entire string, to allow the file to be closed. let n = BS.length s () <- when (n == n) $ return () -- Close the file close -- Force the interface to make sure the interface version is looked at i `seq` return $ Just i -- Catch exceptions and close `catch` \e -> close >> handler e -- Catch exceptions `catch` handler where handler e = case e of ErrorCall _ -> return Nothing IOException e -> do UTF8.putStrLn $ "IO exception: " ++ show e return Nothing -- work-around for file locking bug _ -> throwIO e writeInterface :: FilePath -> Interface -> IO () writeInterface file i = do encodeFile file i `catch` \e -> do UTF8.putStrLn $ "failed to write interface " ++ file ++ " : " ++ show e removeFile file return () -- | Return type used by 'createInterface'. data CreateInterfaceResult = Success { cirVisited :: VisitedModules , cirDecoded :: DecodedModules , cirInterface :: Interface , cirSignature :: Signature , cirBuiltin :: BuiltinThings PrimFun } -- ^ Everything completed successfully, and an interface file was -- written. | Warnings { terminationProblems :: [([QName], [R.Range])] , unsolvedMetaVariables :: [Range] } -- ^ Type checking was successful, except for some termination -- checking problems or unsolved meta-variables. -- -- Meta-variable problems are reported as type errors unless we -- are type checking a top-level module and the flag to allow -- unsolved meta-variables has been selected. -- | Tries to type check a module and write out its interface. -- -- If appropriate this function writes out syntax highlighting -- information. createInterface :: CommandLineOptions -> CallTrace -> [ModuleName] -> VisitedModules -> DecodedModules -> Signature -> BuiltinThings PrimFun -> Maybe ModuleName -- ^ Expected module name. -> FilePath -- ^ The file to type check. -- Must be an absolute path. -> Bool -- ^ Should the working directory be -- changed to the root directory of -- the \"project\" containing the -- file? -> TCM (TopLevelInfo, CreateInterfaceResult) createInterface opts trace path visited decoded isig ibuiltin mname file changeDir | not (isAbsolute file) = __IMPOSSIBLE__ | otherwise = withImportPath path $ do setDecodedModules decoded setTrace trace setCommandLineOptions opts setVisitedModules visited reportSLn "" 1 $ "Checking " ++ (case mname of Nothing -> file Just m -> show m ++ " (" ++ file ++ ")") ++ "." reportSLn "import.iface.create" 5 $ "Creating interface for " ++ show mname reportSLn "import.iface.create" 10 $ " visited: " ++ show (Map.keys visited) addImportedThings isig ibuiltin (pragmas, top) <- liftIO $ parseFile' moduleParser file when changeDir $ liftIO $ setWorkingDirectory file top pragmas <- concat <$> concreteToAbstract_ pragmas -- identity for top-level pragmas at the moment -- Note that pragmas can affect scope checking. setOptionsFromPragmas pragmas topLevel <- concreteToAbstract_ (TopLevel top) termErrs <- catchError (do checkModuleName mname file topLevel -- Type checking. checkDecls (topLevelDecls topLevel) -- Termination checking. termErrs <- ifM (optTerminationCheck <$> commandLineOptions) (termDecls $ topLevelDecls topLevel) (return []) mapM_ (\e -> reportSLn "term.warn.no" 2 (show (fst e) ++ " does NOT pass the termination checker.")) termErrs return termErrs ) (\e -> do -- If there is an error syntax highlighting info can still be -- generated. Since there is no Vim highlighting for errors no -- Vim highlighting is generated, though. whenM (optGenerateEmacsFile <$> commandLineOptions) $ do writeEmacsFile =<< generateSyntaxInfo file TypeCheckingNotDone topLevel [] throwError e) -- Generate syntax highlighting info. syntaxInfo <- generateSyntaxInfo file TypeCheckingDone topLevel termErrs -- Write Emacs file. whenM (optGenerateEmacsFile <$> commandLineOptions) $ writeEmacsFile syntaxInfo -- Generate Vim file. whenM (optGenerateVimFile <$> commandLineOptions) $ withScope_ (insideScope topLevel) $ generateVimFile file -- Check if there are unsolved meta-variables. unsolvedMetas <- List.nub <$> (mapM getMetaRange =<< getOpenMetas) case unsolvedMetas of [] -> return () _ -> do unsolvedOK <- optAllowUnsolved <$> commandLineOptions unless (unsolvedOK && path == []) $ do typeError $ UnsolvedMetas unsolvedMetas setScope $ outsideScope topLevel reportSLn "scope.top" 50 $ "SCOPE " ++ show (insideScope topLevel) -- True if the file was successfully and completely -- type-checked. let ok = null termErrs && null unsolvedMetas (,) topLevel <$> if ok then do i <- buildInterface (topLevelModuleName topLevel) syntaxInfo isig <- getImportedSignature vs <- getVisitedModules ds <- getDecodedModules ibuiltin <- gets stImportedBuiltins liftIO $ writeInterface (toIFile file) i modify (\s -> s { stCurrentModule = Just (topLevelModuleName topLevel, i) }) return (Success vs ds i isig ibuiltin) else return (Warnings termErrs unsolvedMetas) -- | Builds an interface for the current module, which should already -- have been successfully type checked. buildInterface :: ModuleName -- ^ The name of the current module. -> HighlightingInfo -- ^ Syntax highlighting info for the module. -> TCM Interface buildInterface m syntaxInfo = do reportSLn "import.iface" 5 "Building interface..." scope' <- getScope let scope = scope' { scopeCurrent = m } sig <- getSignature builtin <- gets stLocalBuiltins ms <- getImports hsImps <- getHaskellImports let builtin' = Map.mapWithKey (\x b -> fmap (const x) b) builtin reportSLn "import.iface" 7 " instantiating all meta variables" i <- instantiateFull $ Interface { iImportedModules = Set.toList ms , iModuleName = m , iScope = publicModules scope , iSignature = sig , iBuiltin = builtin' , iHaskellImports = hsImps , iHighlighting = syntaxInfo } reportSLn "import.iface" 7 " interface complete" return i -- | Set the current working directory based on the file name of the -- current module and its module name, so that when the module is -- imported qualified it will be found. -- -- The given list of declarations should correspond to a module, i.e. -- it should be non-empty and the last declaration should be -- 'C.Module' something. setWorkingDirectory :: FilePath -> [C.Declaration] -> IO () setWorkingDirectory _ [] = __IMPOSSIBLE__ setWorkingDirectory file xs = case last xs of C.Module _ n _ _ -> do -- canonicalizePath seems to return absolute paths. absolute <- canonicalizePath file let (path, _, _) = splitFilePath absolute setCurrentDirectory (dropDirectory (countDots n) path) _ -> __IMPOSSIBLE__ where countDots (C.QName _) = 0 countDots (C.Qual _ n) = 1 + countDots n -- | Move somewhere else. matchFileName :: ModuleName -> FilePath -> Bool matchFileName mname file = expected `isSuffixOf` given || literate `isSuffixOf` given where given = splitPath file expected = splitPath $ C.moduleNameToFileName (mnameToConcrete mname) ".agda" literate = splitPath $ C.moduleNameToFileName (mnameToConcrete mname) ".lagda" -- | Checks that the top-level module name, the file name and what we -- expect are consistent. checkModuleName :: Maybe ModuleName -- ^ The module should have this name (might be -- unknown). -> FilePath -- ^ The module is defined in this file, whose -- file name is known to be consistent with the -- previous argument. -> TopLevelInfo -- ^ The module. -> TCM () checkModuleName mname file topLevel = case mname of Nothing -> unless (matchFileName actualName file) err Just expectedName -> unless (actualName == expectedName) err where actualName = topLevelModuleName topLevel err = typeError $ ModuleNameDoesntMatchFileName actualName -- | True if the first file is newer than the second file. If a file doesn't -- exist it is considered to be infinitely old. isNewerThan :: FilePath -> FilePath -> IO Bool isNewerThan new old = do newExist <- doesFileExist new oldExist <- doesFileExist old if not (newExist && oldExist) then return newExist else do newT <- getModificationTime new oldT <- getModificationTime old return $ newT >= oldT