{- Copyright 2016, Dominic Orchard, Andrew Rice, Mistral Contrastin, Matthew Danish Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -} {- This module collects together stubs that connect analysis/transformations with the input -> output procedures -} {-# LANGUAGE DoAndIfThenElse #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeSynonymInstances #-} module Camfort.Functionality ( -- * Datatypes AnnotationType(..) , CamfortEnv(..) -- * Commands , ast , countVarDecls , implicitNone , allocCheck , fpCheck , useCheck , arrayCheck -- ** Stencil Analysis , stencilsCheck , stencilsInfer , stencilsSynth -- ** Unit Analysis , unitsCriticals , unitsCheck , unitsDump , unitsInfer , unitsCompile , unitsSynth -- ** Invariants Analysis , invariantsCheck -- ** Refactorings , common , dead , equivalences , ddtRefactor , ddtInfer , ddtCheck , ddtSynth , ddtCompile -- ** Project Management , camfortInitialize ) where import Camfort.Analysis import Camfort.Analysis.Logger import Camfort.Analysis.ModFile (readParseSrcFile, MFCompiler, getModFiles, genModFiles , readParseSrcDir, readParseSrcDirP, simpleCompiler) import Camfort.Analysis.Simple import Camfort.Helpers (FileOrDir, Filename) import Camfort.Input import qualified Camfort.Specification.DerivedDataType as DDT import qualified Camfort.Specification.Hoare as Hoare import qualified Camfort.Specification.Stencils as Stencils import Camfort.Specification.Stencils.Analysis (compileStencils) import qualified Camfort.Specification.Units as LU import Camfort.Specification.Units.Analysis (compileUnits) import Camfort.Specification.Units.Analysis.Consistent (checkUnits) import Camfort.Specification.Units.Analysis.Criticals (inferCriticalVariables) import Camfort.Specification.Units.Analysis.Infer (InferenceResult(..), InferenceReport(..), inferUnits) import Camfort.Specification.Units.Annotation (unitInfo) import Camfort.Specification.Units.ModFile (dumpModFileCompiledUnits) import Camfort.Specification.Units.Monad (runUnitAnalysis, unitOpts0) import Camfort.Specification.Units.MonadTypes (LiteralsOpt, UnitAnalysis, UnitEnv (..), UnitOpts (..)) import Camfort.Transformation.CommonBlockElim import Camfort.Transformation.DeadCode import Camfort.Transformation.EquivalenceElim import Control.DeepSeq import Control.Monad import Control.Monad.IO.Class import qualified Data.ByteString.Lazy as LB import Data.Char (toLower) import Data.List (intersperse) import Data.Maybe (fromMaybe) import qualified Language.Fortran.Analysis as FA import qualified Language.Fortran.Analysis.ModGraph as FM import qualified Language.Fortran.Analysis.Renaming as FA import Language.Fortran.ParserMonad (FortranVersion(..)) import qualified Language.Fortran.Util.ModFile as FM import Pipes import qualified Pipes.Prelude as P import Prelude hiding (mod) import System.Directory import System.Directory (doesDirectoryExist, createDirectoryIfMissing, getCurrentDirectory) import System.FilePath (takeDirectory, (), takeExtension, replaceExtension) import System.IO import Text.PrettyPrint.GenericPretty (pp) data AnnotationType = ATDefault | Doxygen | Ford -- | Retrieve the marker character compatible with the given -- type of annotation. markerChar :: AnnotationType -> Char markerChar Doxygen = '<' markerChar Ford = '!' markerChar ATDefault = '=' data CamfortEnv = CamfortEnv { ceInputSources :: FileOrDir , ceIncludeDir :: Maybe FileOrDir , ceExcludeFiles :: [Filename] , ceLogLevel :: LogLevel , ceSourceSnippets :: Bool , ceFortranVersion :: Maybe FortranVersion } -------------------------------------------------------------------------------- -- * Running Functionality runWithOutput :: (Describe e, Describe w) => String -- ^ Functionality desription -> AnalysisProgram e w IO a b -- ^ Analysis program -> (FileOrDir -> FilePath -> AnalysisRunner e w IO a b r) -- ^ Analysis runner -> MFCompiler i IO -- ^ Mod file compiler -> i -- ^ Mod file input -> FilePath -> CamfortEnv -> IO r runWithOutput description program runner mfCompiler mfInput outSrc env = let runner' = runner (ceInputSources env) outSrc in runFunctionality description program runner' mfCompiler mfInput env runFunctionality :: (Describe e, Describe w) => String -- ^ Functionality desription -> AnalysisProgram e w IO a b -- ^ Analysis program -> AnalysisRunner e w IO a b r -- ^ Analysis runner -> MFCompiler i IO -- ^ Mod file compiler -> i -- ^ Mod file input -> CamfortEnv -> IO r runFunctionality description program runner _ _ env = do putStrLn $ description ++ " '" ++ ceInputSources env ++ "'" incDir' <- maybe getCurrentDirectory pure (ceIncludeDir env) isDir <- doesDirectoryExist incDir' let incDir | isDir = incDir' | otherwise = takeDirectory incDir' -- Previously... --modFiles <- genModFiles mfCompiler mfInput incDir (ceExcludeFiles env) -- ...instead for now, just get the mod files modFiles <- getModFiles incDir pfsTexts <- readParseSrcDir (ceFortranVersion env) modFiles (ceInputSources env) (ceExcludeFiles env) runner program (logOutputStd True) (ceLogLevel env) (ceSourceSnippets env) modFiles pfsTexts runFunctionalityP :: (Describe e, Describe w, ExitCodeOfReport b) => String -- ^ Functionality desription -> AnalysisProgram e w IO a b -- ^ Analysis program -> AnalysisRunnerP e w IO a b (AnalysisReport e w b) -- ^ Analysis runner -> MFCompiler i IO -- ^ Mod file compiler -> i -- ^ Mod file input -> CamfortEnv -> IO Int runFunctionalityP description program runner _ _ env = do putStrLn $ description ++ " '" ++ ceInputSources env ++ "'" incDir' <- maybe getCurrentDirectory pure (ceIncludeDir env) isDir <- doesDirectoryExist incDir' let incDir | isDir = incDir' | otherwise = takeDirectory incDir' -- Previously... --modFiles <- genModFiles mfCompiler mfInput incDir (ceExcludeFiles env) -- ...instead for now, just get the mod files modFiles <- getModFiles incDir reports <- P.toListM $ readParseSrcDirP (ceFortranVersion env) modFiles (ceInputSources env) (ceExcludeFiles env) >-> runner program (logOutputStd True) (ceLogLevel env) (ceSourceSnippets env) modFiles return . exitCodeOfSet $ reports -------------------------------------------------------------------------------- -- * Wrappers on all of the features ast :: CamfortEnv -> IO () ast env = do incDir' <- maybe getCurrentDirectory pure (ceIncludeDir env) modFiles <- getModFiles incDir' xs <- readParseSrcDir (ceFortranVersion env) modFiles (ceInputSources env) (ceExcludeFiles env) print . fmap fst $ xs countVarDecls :: CamfortEnv -> IO Int countVarDecls = runFunctionalityP "Counting variable declarations in" (generalizePureAnalysis . countVariableDeclarations) (describePerFileAnalysisP "count variable declarations") simpleCompiler () dead :: FileOrDir -> CamfortEnv -> IO Int dead = runWithOutput "Eliminating dead code in" (fmap generalizePureAnalysis . perFileRefactoring $ deadCode False) (doRefactor "dead code elimination") simpleCompiler () common :: FileOrDir -> CamfortEnv -> IO Int common outSrc = runWithOutput "Refactoring common blocks in" (generalizePureAnalysis . commonElimToModules (takeDirectory outSrc ++ "/")) (doRefactorAndCreate "common block refactoring") simpleCompiler () outSrc equivalences :: FileOrDir -> CamfortEnv -> IO Int equivalences = runWithOutput "Refactoring equivalences blocks in" (fmap generalizePureAnalysis . perFileRefactoring $ refactorEquivalences) (doRefactor "equivalence block refactoring") simpleCompiler () implicitNone :: Bool -> CamfortEnv -> IO Int implicitNone allPU = runFunctionalityP "Checking 'implicit none' completeness" (generalizePureAnalysis . (checkImplicitNone allPU)) (describePerFileAnalysisP "check 'implicit none'") simpleCompiler () allocCheck :: CamfortEnv -> IO Int allocCheck = runFunctionalityP "Checking allocate / deallocate usage" (generalizePureAnalysis . checkAllocateStatements) (describePerFileAnalysisP "check allocate / deallocate") simpleCompiler () fpCheck :: CamfortEnv -> IO Int fpCheck = runFunctionalityP "Checking usage of floating point" (generalizePureAnalysis . checkFloatingPointUse) (describePerFileAnalysisP "check floating point") simpleCompiler () useCheck :: CamfortEnv -> IO Int useCheck = runFunctionalityP "Checking usage of USE statements" (generalizePureAnalysis . checkModuleUse) (describePerFileAnalysisP "check module USE") simpleCompiler () arrayCheck :: CamfortEnv -> IO Int arrayCheck = runFunctionalityP "Checking array usage" (generalizePureAnalysis . checkArrayUse) (describePerFileAnalysisP "check array usage") simpleCompiler () ddtRefactor :: FileOrDir -> CamfortEnv -> IO Int ddtRefactor = runWithOutput "Refactoring derived datatypes" (generalizePureAnalysis . DDT.refactor) (doRefactor "infer derived data types") simpleCompiler () ddtSynth :: AnnotationType -> FileOrDir -> CamfortEnv -> IO Int ddtSynth annType = runWithOutput "Synthesising derived datatypes" (generalizePureAnalysis . DDT.synth (markerChar annType)) (doRefactor "synth derived data types") simpleCompiler () ddtCheck :: CamfortEnv -> IO Int ddtCheck = runFunctionalityP "Checking derived datatype annotations" (generalizePureAnalysis . DDT.check) (describePerFileAnalysisP "check derived datatypes") simpleCompiler () ddtInfer :: CamfortEnv -> IO Int ddtInfer = runFunctionalityP "Inferring derived datatypes" (generalizePureAnalysis . DDT.infer) (describePerFileAnalysisP "infer derived datatypes") simpleCompiler () ddtCompile :: CamfortEnv -> IO Int ddtCompile env = do let description = "Compiling derived datatypes for" putStrLn $ description ++ " '" ++ ceInputSources env ++ "'" incDir' <- maybe getCurrentDirectory pure (ceIncludeDir env) isDir <- doesDirectoryExist incDir' let incDir | isDir = incDir' | otherwise = takeDirectory incDir' modFileNames <- getModFiles incDir -- Run the gen mod file routine directly on the input source modFiles <- genModFiles (ceFortranVersion env) modFileNames DDT.compile () (ceInputSources env) (ceExcludeFiles env) -- Write the mod files out forM_ modFiles $ \ modFile -> do let mfname = replaceExtension (FM.moduleFilename modFile) FM.modFileSuffix LB.writeFile mfname (FM.encodeModFile [modFile]) return 0 {- Units feature -} runUnitsFunctionalityP :: (Describe e, Describe w, ExitCodeOfReport b) => String -> (UnitOpts -> AnalysisProgram e w IO a b) -> AnalysisRunnerP e w IO a b (AnalysisReport e w b) -> LiteralsOpt -> CamfortEnv -> IO Int runUnitsFunctionalityP description unitsProgram runner opts = let uo = optsToUnitOpts opts in runFunctionalityP description (unitsProgram uo) runner compileUnits uo optsToUnitOpts :: LiteralsOpt -> UnitOpts optsToUnitOpts m = o1 where o1 = unitOpts0 { uoLiterals = m } singlePfUnits :: UnitAnalysis a -> UnitOpts -> AnalysisProgram () () IO ProgramFile a singlePfUnits unitAnalysis opts pf = let ue = UnitEnv { unitOpts = opts , unitProgramFile = pf } in runUnitAnalysis ue unitAnalysis -- slight hack to make doRefactorAndCreate happy -- singlePfUnits' -- :: UnitAnalysis a -> UnitOpts -- -> AnalysisProgram () () IO [ProgramFile] a -- singlePfUnits' unitAnalysis opts (pf:_) = -- let ue = UnitEnv -- { unitOpts = opts -- , unitProgramFile = pf -- } -- in runUnitAnalysis ue unitAnalysis -- singlePfUnits' _ _ [] = error "singlePfUnits': no program file provided" multiPfUnits :: (Describe a) => UnitAnalysis (Either e (a, b)) -> UnitOpts -> AnalysisProgram () () IO [ProgramFile] (Text, [Either e b]) multiPfUnits unitAnalysis opts pfs = do let ue pf = UnitEnv { unitOpts = opts , unitProgramFile = pf } results <- traverse (\pf -> runUnitAnalysis (ue pf) unitAnalysis) pfs let (rs, ps) = traverse (traverse (\(x, y) -> ([x], y))) results rs' = mconcat . intersperse "\n" . map describe $ rs return (rs', ps) unitsDump :: LiteralsOpt -> CamfortEnv -> IO Int unitsDump _ env = do let modFileName = ceInputSources env modData <- LB.readFile modFileName let eResult = FM.decodeModFile modData case eResult of Left msg -> do putStrLn $ modFileName ++ ": Error: " ++ show msg pure 1 Right modFiles -> do forM_ modFiles $ \ modFile -> do putStrLn $ modFileName ++ ": successfully parsed summary file." putStrLn . fromMaybe "unable to find units info" $ dumpModFileCompiledUnits modFile pure 0 unitsCheck :: LiteralsOpt -> CamfortEnv -> IO Int unitsCheck = runUnitsFunctionalityP "Checking units for" (singlePfUnits checkUnits) (describePerFileAnalysisP "unit checking") unitsInfer :: Bool -> LiteralsOpt -> CamfortEnv -> IO Int unitsInfer showAST = runUnitsFunctionalityP "Inferring units for" (singlePfUnits inferUnits) (if showAST then describePerFileAnalysisShowASTUnitInfoP else describePerFileAnalysisP "unit inference") -- | Given an analysis program for a single file, run it over every input file -- and collect the reports, then print those reports to standard output. describePerFileAnalysisShowASTUnitInfoP :: (MonadIO m, Describe w, Describe e, NFData w, NFData e) => AnalysisRunnerP e w m ProgramFile InferenceResult (AnalysisReport e w InferenceResult) describePerFileAnalysisShowASTUnitInfoP program logOutput logLevel snippets modFiles = runPerFileAnalysisP program logOutput logLevel snippets modFiles >-> (P.mapM $ \ r -> do case r of AnalysisReport _ _ (ARSuccess (Inferred (InferenceReport pfUA _))) -> liftIO . pp . fmap (unitInfo . FA.prevAnnotation) . FA.rename $ pfUA _ -> pure () putDescribeReport "unit inference" (Just logLevel) snippets r pure r) -- | Expand all paths that are directories into a list of Fortran -- files from a recursive directory listing. expandDirs :: [FilePath] -> IO [FilePath] expandDirs = fmap concat . mapM doEach where doEach path = do isDir <- doesDirectoryExist path if isDir then listFortranFiles path else pure [path] -- | Get a list of Fortran files under the given directory. listFortranFiles :: FilePath -> IO [FilePath] listFortranFiles dir = filter isFortran <$> listDirectoryRecursively dir where -- | True if the file has a valid fortran extension. isFortran :: FilePath -> Bool isFortran x = map toLower (takeExtension x) `elem` exts where exts = [".f", ".f90", ".f77", ".f03"] listDirectoryRecursively :: FilePath -> IO [FilePath] listDirectoryRecursively dir = listDirectoryRec dir "" where listDirectoryRec :: FilePath -> FilePath -> IO [FilePath] listDirectoryRec d f = do let fullPath = d f isDir <- doesDirectoryExist fullPath if isDir then do conts <- listDirectory fullPath concat <$> mapM (listDirectoryRec fullPath) conts else pure [fullPath] decodeOneModFile :: FilePath -> IO FM.ModFiles decodeOneModFile path = do contents <- LB.readFile path case FM.decodeModFile contents of Left msg -> do hPutStrLn stderr $ path ++ ": Error: " ++ msg return [] Right modFiles -> do hPutStrLn stderr $ path ++ ": successfully parsed summary file." return modFiles unitsCompile :: LiteralsOpt -> CamfortEnv -> IO Int unitsCompile opts env = do let uo = optsToUnitOpts opts let description = "Compiling units for" putStrLn $ description ++ " '" ++ ceInputSources env ++ "'" incDir' <- maybe getCurrentDirectory pure (ceIncludeDir env) isDir <- doesDirectoryExist incDir' let incDir | isDir = incDir' | otherwise = takeDirectory incDir' -- modFileNames <- getModFiles incDir paths' <- expandDirs $ [ceInputSources env] -- Build the graph of module dependencies mg0 <- FM.genModGraph (ceFortranVersion env) [incDir] paths' let compileFileToMod mods pf = do mod <- compileUnits uo mods pf let mfname = replaceExtension (FM.moduleFilename mod) FM.modFileSuffix LB.writeFile mfname (FM.encodeModFile [mod]) pure mod -- Loop through the dependency graph until it is empty let loop mg mods | nxt <- FM.takeNextMods mg , not (null nxt) = do let fnPaths = [ fn | (_, Just (FM.MOFile fn)) <- nxt ] newMods <- fmap concat . forM fnPaths $ \ fnPath -> do tsStatus <- FM.checkTimestamps fnPath case tsStatus of FM.NoSuchFile -> do putStr $ "Does not exist: " ++ fnPath pure [FM.emptyModFile] FM.ModFileExists modPath -> do putStrLn $ "Loading mod file " ++ modPath ++ "." decodeOneModFile modPath FM.CompileFile -> do putStr $ "Summarising " ++ fnPath ++ "..." m_pf <- readParseSrcFile (ceFortranVersion env) mods fnPath case m_pf of Just (pf, _) -> do mod <- compileFileToMod mods pf putStrLn "done" pure [mod] Nothing -> do putStrLn "failed" pure [] let ns = map fst nxt let mg' = FM.delModNodes ns mg loop mg' $ newMods ++ mods loop _ mods = pure mods _allMods <- loop mg0 [] return 0 unitsSynth :: AnnotationType -> FileOrDir -> LiteralsOpt -> CamfortEnv -> IO Int unitsSynth annType outSrc opts env = runFunctionality "Synthesising units for" (multiPfUnits (LU.synthesiseUnits (markerChar annType)) uo) (doRefactor "unit synthesis" (ceInputSources env) outSrc) compileUnits uo env where uo = optsToUnitOpts opts unitsCriticals :: LiteralsOpt -> CamfortEnv -> IO Int unitsCriticals = runUnitsFunctionalityP "Suggesting variables to annotate with unit specifications in" (singlePfUnits inferCriticalVariables) (describePerFileAnalysisP "unit critical variable analysis") {- Stencils feature -} stencilsCheck :: CamfortEnv -> IO Int stencilsCheck = runFunctionalityP "Checking stencil specs for" (generalizePureAnalysis . Stencils.check) (describePerFileAnalysisP "stencil checking") compileStencils () stencilsInfer :: Bool -> CamfortEnv -> IO Int stencilsInfer useEval = runFunctionalityP "Inferring stencil specs for" (generalizePureAnalysis . Stencils.infer useEval '=') (describePerFileAnalysisP "stencil inference") compileStencils () stencilsSynth :: AnnotationType -> FileOrDir -> CamfortEnv -> IO Int stencilsSynth annType = let program :: AnalysisProgram () () IO [ProgramFile] ((), [Either () ProgramFile]) program pfs = generalizePureAnalysis $ do pfs' <- Stencils.synth (markerChar annType) pfs return ((), map Right pfs') in runWithOutput "Synthesising stencil specs for" program (doRefactor "stencil synthesis") compileStencils () {- Invariants Feature-} invariantsCheck :: Hoare.PrimReprOption -> CamfortEnv -> IO Int invariantsCheck pro = runFunctionalityP "Checking invariants in" (Hoare.check pro) (describePerFileAnalysisP "invariant checking") simpleCompiler () -- | Initialize Camfort for the given project. camfortInitialize :: FilePath -> IO () camfortInitialize projectDir = createDirectoryIfMissing False (projectDir ".camfort")