{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -- | Low-level compilation parts. Look at "Futhark.Compiler" for a -- more high-level API. module Futhark.Compiler.Program ( readLibrary, readUntypedLibrary, Imports, FileModule (..), E.Warnings, LoadedProg (lpNameSource), noLoadedProg, lpImports, reloadProg, extendProg, ) where import Control.Concurrent (forkIO) import Control.Concurrent.MVar ( MVar, modifyMVar, newEmptyMVar, newMVar, putMVar, readMVar, ) import Control.Monad import Control.Monad.Except import Control.Monad.State (execStateT, gets, modify) import Data.Bifunctor (first) import Data.List (intercalate, isPrefixOf, sort) import qualified Data.Map as M import Data.Maybe (mapMaybe) import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Time.Clock (UTCTime) import Futhark.Error import Futhark.FreshNames import Futhark.Util (interactWithFileSafely, nubOrd, startupTime) import Futhark.Util.Pretty (line, ppr, text, ()) import qualified Language.Futhark as E import Language.Futhark.Parser import Language.Futhark.Prelude import Language.Futhark.Semantic import qualified Language.Futhark.TypeChecker as E import Language.Futhark.Warnings import System.Directory (getModificationTime) import System.FilePath (normalise) import qualified System.FilePath.Posix as Posix data LoadedFile fm = LoadedFile { lfPath :: FilePath, lfImportName :: ImportName, lfMod :: fm, -- | Modification time of the underlying file. lfModTime :: UTCTime } deriving (Eq, Ord, Show) newtype UncheckedImport = UncheckedImport { unChecked :: Either CompilerError (LoadedFile E.UncheckedProg, [(ImportName, MVar UncheckedImport)]) } -- | If mapped to Nothing, treat it as present. This is used when -- reloading programs. type ReaderState = MVar (M.Map ImportName (Maybe (MVar UncheckedImport))) newState :: [ImportName] -> IO ReaderState newState known = newMVar $ M.fromList $ zip known $ repeat Nothing orderedImports :: (MonadError CompilerError m, MonadIO m) => [(ImportName, MVar UncheckedImport)] -> m [(ImportName, LoadedFile E.UncheckedProg)] orderedImports = fmap reverse . flip execStateT [] . mapM_ (spelunk []) where spelunk steps (include, mvar) | include `elem` steps = externalErrorS $ "Import cycle: " ++ intercalate " -> " (map includeToString $ reverse $ include : steps) | otherwise = do prev <- gets $ lookup include case prev of Just _ -> pure () Nothing -> do (file, more_imports) <- either throwError pure . unChecked =<< liftIO (readMVar mvar) mapM_ (spelunk (include : steps)) more_imports modify ((include, file) :) newImportMVar :: IO UncheckedImport -> IO (Maybe (MVar UncheckedImport)) newImportMVar m = do mvar <- newEmptyMVar void $ forkIO $ putMVar mvar =<< m pure $ Just mvar contentsAndModTime :: FilePath -> IO (Maybe (Either String (T.Text, UTCTime))) contentsAndModTime filepath = interactWithFileSafely $ (,) <$> T.readFile filepath <*> getModificationTime filepath readImportFile :: ImportName -> IO (Either CompilerError (LoadedFile T.Text)) readImportFile include = do -- First we try to find a file of the given name in the search path, -- then we look at the builtin library if we have to. For the -- builtins, we don't use the search path. let filepath = includeToFilePath include r <- contentsAndModTime filepath case (r, lookup prelude_str prelude) of (Just (Right (s, mod_time)), _) -> pure $ Right $ loaded filepath s mod_time (Just (Left e), _) -> pure $ Left $ ExternalError $ text e (Nothing, Just s) -> pure $ Right $ loaded prelude_str s startupTime (Nothing, Nothing) -> pure $ Left $ ExternalError $ text not_found where prelude_str = "/" Posix. includeToString include Posix.<.> "fut" loaded path s mod_time = LoadedFile { lfImportName = include, lfPath = path, lfMod = s, lfModTime = mod_time } not_found = "Error at " ++ E.locStr (E.srclocOf include) ++ ": could not find import '" ++ includeToString include ++ "'." handleFile :: ReaderState -> LoadedFile T.Text -> IO UncheckedImport handleFile state_mvar (LoadedFile file_name import_name file_contents mod_time) = do case parseFuthark file_name file_contents of Left err -> pure $ UncheckedImport $ Left $ ExternalError $ text $ show err Right prog -> do let imports = map (uncurry (mkImportFrom import_name)) $ E.progImports prog mvars <- mapMaybe sequenceA . zip imports <$> mapM (readImport state_mvar) imports let file = LoadedFile { lfPath = file_name, lfImportName = import_name, lfModTime = mod_time, lfMod = prog } pure $ UncheckedImport $ Right (file, mvars) readImport :: ReaderState -> ImportName -> IO (Maybe (MVar UncheckedImport)) readImport state_mvar include = modifyMVar state_mvar $ \state -> case M.lookup include state of Just x -> pure (state, x) Nothing -> do prog_mvar <- newImportMVar $ do readImportFile include >>= \case Left e -> pure $ UncheckedImport $ Left e Right file -> handleFile state_mvar file pure (M.insert include prog_mvar state, prog_mvar) readUntypedLibraryExceptKnown :: (MonadIO m, MonadError CompilerError m) => [ImportName] -> [FilePath] -> m [LoadedFile E.UncheckedProg] readUntypedLibraryExceptKnown known fps = do state_mvar <- liftIO $ newState known let prelude_import = mkInitialImport "/prelude/prelude" prelude_mvar <- liftIO $ readImport state_mvar prelude_import fps_mvars <- liftIO (mapM (onFile state_mvar) fps) let unknown_mvars = onlyUnknown ((prelude_import, prelude_mvar) : fps_mvars) map snd <$> orderedImports unknown_mvars where onlyUnknown = mapMaybe sequenceA onFile state_mvar fp = modifyMVar state_mvar $ \state -> do case M.lookup include state of Just prog_mvar -> pure (state, (include, prog_mvar)) Nothing -> do prog_mvar <- newImportMVar $ do r <- contentsAndModTime fp case r of Just (Right (fs, mod_time)) -> do handleFile state_mvar $ LoadedFile { lfImportName = include, lfMod = fs, lfModTime = mod_time, lfPath = fp } Just (Left e) -> pure $ UncheckedImport $ Left $ ExternalError $ text $ show e Nothing -> pure $ UncheckedImport $ Left $ ExternalError $ text $ fp <> ": file not found." pure (M.insert include prog_mvar state, (include, prog_mvar)) where include = mkInitialImport fp_name (fp_name, _) = Posix.splitExtension fp asImports :: [LoadedFile (VNameSource, FileModule)] -> Imports asImports = map f where f lf = (includeToString (lfImportName lf), snd $ lfMod lf) typeCheckProg :: MonadError CompilerError m => [LoadedFile (VNameSource, FileModule)] -> VNameSource -> [LoadedFile E.UncheckedProg] -> m (E.Warnings, [LoadedFile (VNameSource, FileModule)], VNameSource) typeCheckProg orig_imports orig_src = foldM f (mempty, orig_imports, orig_src) where roots = ["/prelude/prelude"] f (ws, imports, src) (LoadedFile path import_name prog mod_time) = do let prog' | "/prelude" `isPrefixOf` includeToFilePath import_name = prog | otherwise = prependRoots roots prog case E.checkProg (asImports imports) src import_name prog' of (prog_ws, Left err) -> do let ws' = ws <> prog_ws externalError $ if anyWarnings ws' then ppr ws' line <> ppr err else ppr err (prog_ws, Right (m, src')) -> pure ( ws <> prog_ws, imports ++ [LoadedFile path import_name (src, m) mod_time], src' ) setEntryPoints :: [E.Name] -> [FilePath] -> [LoadedFile E.UncheckedProg] -> [LoadedFile E.UncheckedProg] setEntryPoints extra_eps fps = map onFile where fps' = map normalise fps onFile lf | includeToFilePath (lfImportName lf) `elem` fps' = lf {lfMod = prog {E.progDecs = map onDec (E.progDecs prog)}} | otherwise = lf where prog = lfMod lf onDec (E.ValDec vb) | E.valBindName vb `elem` extra_eps = E.ValDec vb {E.valBindEntryPoint = Just E.NoInfo} onDec dec = dec prependRoots :: [FilePath] -> E.UncheckedProg -> E.UncheckedProg prependRoots roots (E.Prog doc ds) = E.Prog doc $ map mkImport roots ++ ds where mkImport fp = -- We do not use ImportDec here, because we do not want the -- type checker to issue a warning about a redundant import. E.LocalDec (E.OpenDec (E.ModImport fp E.NoInfo mempty) mempty) mempty -- | A loaded, type-checked program. This can be used to extract -- information about the program, but also to speed up subsequent -- reloads. data LoadedProg = LoadedProg { lpRoots :: [FilePath], -- | The 'VNameSource' is the name source just *before* the module -- was type checked. lpFiles :: [LoadedFile (VNameSource, FileModule)], -- | Final name source. lpNameSource :: VNameSource } -- | The 'Imports' of a 'LoadedProg', as expected by e.g. type -- checking functions. lpImports :: LoadedProg -> Imports lpImports = map f . lpFiles where f lf = (includeToString (lfImportName lf), snd $ lfMod lf) unchangedImports :: MonadIO m => VNameSource -> [LoadedFile (VNameSource, FileModule)] -> m ([LoadedFile (VNameSource, FileModule)], VNameSource) unchangedImports src [] = pure ([], src) unchangedImports src (f : fs) | "/prelude" `isPrefixOf` includeToFilePath (lfImportName f) = first (f :) <$> unchangedImports src fs | otherwise = do changed <- maybe True (either (const True) (> lfModTime f)) <$> liftIO (interactWithFileSafely (getModificationTime $ lfPath f)) if changed then pure ([], fst $ lfMod f) else first (f :) <$> unchangedImports src fs -- | A "loaded program" containing no actual files. Use this as a -- starting point for 'reloadProg' noLoadedProg :: LoadedProg noLoadedProg = LoadedProg { lpRoots = [], lpFiles = mempty, lpNameSource = newNameSource $ E.maxIntrinsicTag + 1 } -- | Find out how many of the old imports can be used. Here we are -- forced to be overly conservative, because our type checker -- enforces a linear ordering. usableLoadedProg :: MonadIO m => LoadedProg -> [FilePath] -> m LoadedProg usableLoadedProg (LoadedProg roots imports src) new_roots | sort roots == sort new_roots = do (imports', src') <- unchangedImports src imports pure $ LoadedProg [] imports' src' | otherwise = pure noLoadedProg -- | Extend a loaded program with (possibly new) files. extendProg :: (MonadError CompilerError m, MonadIO m) => LoadedProg -> [FilePath] -> m (E.Warnings, LoadedProg) extendProg lp new_roots = do new_imports_untyped <- readUntypedLibraryExceptKnown (map lfImportName $ lpFiles lp) new_roots (ws, imports, src') <- typeCheckProg (lpFiles lp) (lpNameSource lp) new_imports_untyped pure (ws, LoadedProg (nubOrd (lpRoots lp ++ new_roots)) imports src') -- | Load some new files, reusing as much of the previously loaded -- program as possible. This does not *extend* the currently loaded -- program the way 'extendProg' does it, so it is always correct (if -- less efficient) to pass 'noLoadedProg'. reloadProg :: (MonadError CompilerError m, MonadIO m) => LoadedProg -> [FilePath] -> m (E.Warnings, LoadedProg) reloadProg lp new_roots = do lp' <- usableLoadedProg lp new_roots extendProg lp' new_roots -- | Read and type-check some Futhark files. readLibrary :: (MonadError CompilerError m, MonadIO m) => -- | Extra functions that should be marked as entry points; only -- applies to the immediate files, not any imports imported. [E.Name] -> -- | The files to read. [FilePath] -> m (E.Warnings, Imports, VNameSource) readLibrary extra_eps fps = fmap frob . typeCheckProg mempty (lpNameSource noLoadedProg) . setEntryPoints (E.defaultEntryPoint : extra_eps) fps =<< readUntypedLibraryExceptKnown [] fps where frob (x, y, z) = (x, asImports y, z) -- | Read (and parse) all source files (including the builtin prelude) -- corresponding to a set of root files. readUntypedLibrary :: (MonadIO m, MonadError CompilerError m) => [FilePath] -> m [(ImportName, E.UncheckedProg)] readUntypedLibrary = fmap (map f) . readUntypedLibraryExceptKnown [] where f lf = (lfImportName lf, lfMod lf)