packages feed

elm-make-0.1: src/LoadInterfaces.hs

{-# LANGUAGE FlexibleContexts #-}
module LoadInterfaces where

import Control.Monad.Error (MonadError, MonadIO, liftIO, throwError)
import Control.Monad.Reader (MonadReader, ask)
import qualified Data.Graph as Graph
import qualified Data.List as List
import Data.Map ((!))
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import System.Directory (doesFileExist, getModificationTime)

import qualified Elm.Compiler.Module as Module
import qualified Path
import qualified Utils.File as File
import TheMasterPlan
    ( ModuleID(ModuleID), Location(..)
    , ProjectSummary(ProjectSummary), ProjectData(..)
    , BuildSummary(..), BuildData(..)
    )


prepForBuild
    :: (MonadIO m, MonadError String m, MonadReader FilePath m)
    => ProjectSummary Location
    -> m BuildSummary
prepForBuild (ProjectSummary projectData _projectNatives) =
  do  enhancedData <- addInterfaces projectData
      filteredData <- filterStaleInterfaces enhancedData
      return (toBuildSummary filteredData)


--- LOAD INTERFACES -- what has already been compiled?

addInterfaces
    :: (MonadIO m, MonadReader FilePath m, MonadError String m)
    => Map.Map ModuleID (ProjectData Location)
    -> m (Map.Map ModuleID (ProjectData (Location, Maybe Module.Interface)))
addInterfaces projectData =
  do  enhancedData <- mapM maybeLoadInterface (Map.toList projectData)
      return (Map.fromList enhancedData)
      

maybeLoadInterface
    :: (MonadIO m, MonadReader FilePath m, MonadError String m)
    => (ModuleID, ProjectData Location)
    -> m (ModuleID, ProjectData (Location, Maybe Module.Interface))
maybeLoadInterface (moduleID, (ProjectData location deps)) =
  do  cacheRoot <- ask
      let interfacePath = Path.toInterface cacheRoot moduleID
      let sourcePath = Path.toSource location
      fresh <- liftIO (isFresh sourcePath interfacePath)

      maybeInterface <-
          case fresh of
            False -> return Nothing
            True ->
              do  interface <- File.readBinary interfacePath
                  return (Just interface)

      return (moduleID, ProjectData (location, maybeInterface) deps)
                    

isFresh :: FilePath -> FilePath -> IO Bool
isFresh sourcePath interfacePath =
  do  exists <- doesFileExist interfacePath
      case exists of
        False -> return False
        True ->
          do  sourceTime <- getModificationTime sourcePath
              interfaceTime <- getModificationTime interfacePath
              return (sourceTime <= interfaceTime)


-- FILTER STALE INTERFACES -- have files become stale due to other changes?

filterStaleInterfaces
    :: (MonadError String m)
    => Map.Map ModuleID (ProjectData (Location, Maybe Module.Interface))
    -> m (Map.Map ModuleID (ProjectData (Either Location Module.Interface)))
filterStaleInterfaces summary =
  do  sortedNames <- topologicalSort (Map.map projectDependencies summary)
      return (List.foldl' (filterIfStale summary) Map.empty sortedNames)


filterIfStale
    :: Map.Map ModuleID (ProjectData (Location, Maybe Module.Interface))
    -> Map.Map ModuleID (ProjectData (Either Location Module.Interface))
    -> ModuleID
    -> Map.Map ModuleID (ProjectData (Either Location Module.Interface))
filterIfStale enhancedSummary filteredSummary moduleName =
    Map.insert moduleName (ProjectData trueLocation deps) filteredSummary
  where
    (ProjectData (filePath, maybeInterface) deps) =
        enhancedSummary ! moduleName

    trueLocation =
        case maybeInterface of
          Just interface
            | all (haveInterface enhancedSummary) deps ->
                Right interface

          _ -> Left filePath


haveInterface
    :: Map.Map ModuleID (ProjectData (Location, Maybe Module.Interface))
    -> ModuleID
    -> Bool
haveInterface enhancedSummary rawName =
    case filterNativeDeps rawName of
      Nothing -> True
      Just name ->
          case Map.lookup name enhancedSummary of
            Just (ProjectData (_, Just _) _) -> True
            _ -> False


-- FILTER DEPENDENCIES -- which modules actually need to be compiled?

toBuildSummary
    :: Map.Map ModuleID (ProjectData (Either Location Module.Interface))
    -> BuildSummary
toBuildSummary summary =
    BuildSummary
    { blockedModules = Map.map (toBuildData interfaces) locations
    , completedInterfaces = interfaces
    }
  where
    (locations, interfaces) =
        Map.mapEither divide summary

    divide (ProjectData either deps) =
        case either of
          Left location ->
              Left (ProjectData location deps)

          Right interface ->
              Right interface

toBuildData
    :: Map.Map ModuleID Module.Interface
    -> ProjectData Location
    -> BuildData
toBuildData interfaces (ProjectData location dependencies) =
    BuildData blocking location
  where
    blocking =
        Maybe.mapMaybe filterDeps dependencies

    filterDeps :: ModuleID -> Maybe ModuleID
    filterDeps deps =
        filterCachedDeps interfaces =<< filterNativeDeps deps


filterCachedDeps
    :: Map.Map ModuleID Module.Interface
    -> ModuleID
    -> Maybe ModuleID
filterCachedDeps interfaces name =
    case Map.lookup name interfaces of
      Just _interface -> Nothing
      Nothing -> Just name


filterNativeDeps :: ModuleID -> Maybe ModuleID
filterNativeDeps name =
    case name of
      ModuleID (Module.Name ("Native" : _)) _pkg ->
          Nothing

      _ ->
          Just name


-- SORT GRAPHS / CHECK FOR CYCLES

topologicalSort :: (MonadError String m) => Map.Map ModuleID [ModuleID] -> m [ModuleID]
topologicalSort dependencies =
    mapM errorOnCycle components
  where
    components =
        Graph.stronglyConnComp (map toNode (Map.toList dependencies))

    toNode (name, deps) =
        (name, name, deps)

    errorOnCycle scc =
        case scc of
          Graph.AcyclicSCC name -> return name
          Graph.CyclicSCC cycle@(first:_) ->
              throwError $
              "Your dependencies for a cycle:\n\n"
              ++ showCycle first cycle
              ++ "\nYou may need to move some values to a new module to get rid of the cycle."


showCycle :: ModuleID -> [ModuleID] -> String
showCycle first cycle =
  case cycle of
    [] -> ""

    [last] -> 
        "    " ++ idToString last ++ " => " ++ idToString first ++ "\n"

    one:two:rest ->
        "    " ++ idToString one ++ " => " ++ idToString two ++ "\n"
        ++ showCycle first (two:rest)
  where
    idToString (ModuleID moduleName _pkg) =
        Module.nameToString moduleName