elm-make-0.1: src/Build.hs
{-# LANGUAGE FlexibleContexts #-}
module Build where
import Control.Concurrent (ThreadId, myThreadId, forkIO, killThread)
import qualified Control.Concurrent.Chan as Chan
import qualified Control.Exception as Exception
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
import qualified Display
import qualified Elm.Compiler as Compiler
import qualified Elm.Compiler.Module as Module
import qualified Elm.Package.Name as Pkg
import qualified Path
import qualified Utils.File as File
import qualified Utils.Queue as Queue
import qualified TheMasterPlan as TMP
import TheMasterPlan
( ModuleID, Location, PackageID
, BuildSummary(BuildSummary), BuildData(..)
)
data Env = Env
{ maxActiveThreads :: Int
, numTasks :: Int
, resultChan :: Chan.Chan Result
, displayChan :: Chan.Chan Display.Update
, reverseDependencies :: Map.Map ModuleID [ModuleID]
, cachePath :: FilePath
, publicModules :: Set.Set ModuleID
}
data State = State
{ currentState :: CurrentState
, activeThreads :: Set.Set ThreadId
, readyQueue :: Queue.Queue (ModuleID, Location)
, blockedModules :: Map.Map ModuleID BuildData
, completedInterfaces :: Map.Map ModuleID Module.Interface
}
data CurrentState = Wait | Update
data Result
= Error ModuleID Location String
| Success ModuleID Module.Interface String ThreadId
-- HELPERS for ENV and STATE
initEnv
:: Int
-> FilePath
-> [ModuleID]
-> Map.Map ModuleID [ModuleID]
-> BuildSummary
-> IO Env
initEnv numProcessors cachePath publicModules dependencies (BuildSummary blocked _completed) =
do resultChan <- Chan.newChan
displayChan <- Chan.newChan
return $ Env {
maxActiveThreads = numProcessors,
numTasks = Map.size blocked,
resultChan = resultChan,
displayChan = displayChan,
reverseDependencies = reverseGraph dependencies,
cachePath = cachePath,
publicModules = Set.fromList publicModules
}
-- reverse dependencies, "who depends on me?"
reverseGraph :: (Ord a) => Map.Map a [a] -> Map.Map a [a]
reverseGraph graph =
Map.foldrWithKey flipEdges Map.empty graph
where
flipEdges name dependencies reversedGraph =
foldr (insertDependency name) reversedGraph dependencies
insertDependency name dep reversedGraph =
Map.insertWith (++) dep [name] reversedGraph
initState :: BuildSummary -> State
initState (BuildSummary blocked completed) =
State {
currentState = Update,
activeThreads = Set.empty,
readyQueue = Queue.fromList (Map.elems readyModules),
blockedModules = blockedModules,
completedInterfaces = completed
}
where
(readyModules, blockedModules) =
Map.mapEitherWithKey categorize blocked
categorize name buildData@(BuildData blocking location) =
case blocking of
[] -> Left (name, location)
_ -> Right buildData
numIncompleteTasks :: State -> Int
numIncompleteTasks state =
Set.size (activeThreads state)
+ Queue.size (readyQueue state)
+ Map.size (blockedModules state)
-- PARALLEL BUILDS!!!
build :: Int -> PackageID -> FilePath -> [ModuleID] -> Map.Map ModuleID [ModuleID] -> BuildSummary -> IO ()
build numProcessors rootPkg cachePath publicModules dependencies summary =
do env <- initEnv numProcessors cachePath publicModules dependencies summary
forkIO (buildManager env (initState summary))
Display.display (displayChan env) rootPkg 0 (numTasks env)
buildManager :: Env -> State -> IO ()
buildManager env state =
case currentState state of
_ | numIncompleteTasks state == 0 ->
Chan.writeChan (displayChan env) Display.Success
Wait ->
do result <- Chan.readChan (resultChan env)
case result of
Success moduleID interface js threadId ->
do let cache = cachePath env
File.writeBinary (Path.toInterface cache moduleID) interface
writeFile (Path.toObjectFile cache moduleID) js
Chan.writeChan (displayChan env) (Display.Completion moduleID)
buildManager env (registerSuccess env state moduleID interface threadId)
Error moduleID location msg ->
do mapM killThread (Set.toList (activeThreads state))
Chan.writeChan (displayChan env) (Display.Error moduleID location msg)
Update ->
do let interfaces = completedInterfaces state
let compile = buildModule env interfaces
threadIds <- mapM (forkIO . compile) runNow
buildManager env $
state
{ currentState = Wait
, activeThreads = foldr Set.insert (activeThreads state) threadIds
, readyQueue = runLater
}
where
(runNow, runLater) =
Queue.dequeue
(maxActiveThreads env - Set.size (activeThreads state))
(readyQueue state)
progress =
fromIntegral (numIncompleteTasks state) / fromIntegral (numTasks env)
-- WAIT - REGISTER RESULTS
registerSuccess :: Env -> State -> ModuleID -> Module.Interface -> ThreadId -> State
registerSuccess env state name interface threadId =
state
{ currentState =
Update
, activeThreads =
Set.delete threadId (activeThreads state)
, blockedModules =
updatedBlockedModules
, readyQueue =
Queue.enqueue (Maybe.catMaybes readyModules) (readyQueue state)
, completedInterfaces =
Map.insert name interface (completedInterfaces state)
}
where
(updatedBlockedModules, readyModules) =
List.mapAccumR
(updateBlockedModules name interface)
(blockedModules state)
(Maybe.fromMaybe [] (Map.lookup name (reverseDependencies env)))
updateBlockedModules
:: ModuleID
-> Module.Interface
-> Map.Map ModuleID BuildData
-> ModuleID
-> (Map.Map ModuleID BuildData, Maybe (ModuleID, Location))
updateBlockedModules name interface blockedModules potentiallyFreedModule =
case Map.lookup potentiallyFreedModule blockedModules of
Nothing ->
(blockedModules, Nothing)
Just (BuildData blocking location) ->
case filter (/= name) blocking of
[] ->
( Map.delete potentiallyFreedModule blockedModules
, Just (potentiallyFreedModule, location)
)
newBlocking ->
( Map.insert
potentiallyFreedModule
(BuildData newBlocking location)
blockedModules
, Nothing
)
-- UPDATE - BUILD SOME MODULES
buildModule
:: Env
-> Map.Map ModuleID Module.Interface
-> (ModuleID, Location)
-> IO ()
buildModule env interfaces (moduleID, location) =
Exception.catch compile recover
where
(Pkg.Name user project) =
fst (TMP.packageID moduleID)
compile =
do source <- readFile (Path.toSource location)
let ifaces = Map.mapKeysMonotonic TMP.moduleName interfaces
let rawResult = Compiler.compile user project source ifaces
result <-
case rawResult of
Left errorMsg ->
return (Error moduleID location errorMsg)
Right (interface, js) ->
case checkPorts env moduleID interface of
Just msg ->
return (Error moduleID location msg)
Nothing ->
do threadId <- myThreadId
return (Success moduleID interface js threadId)
Chan.writeChan (resultChan env) result
recover :: Exception.SomeException -> IO ()
recover msg =
Chan.writeChan (resultChan env) (Error moduleID location (show msg))
checkPorts :: Env -> ModuleID -> Module.Interface -> Maybe String
checkPorts env moduleID interface =
case Set.member moduleID (publicModules env) of
True -> Nothing
False ->
case Module.interfacePorts interface of
[] -> Nothing
_ -> Just portError
portError :: String
portError =
concat
[ "Port Error: ports may only appear in the main module. We do not want ports\n"
, " appearing in library code where it adds a non-modular dependency. If I\n"
, " import it twice, what does that really mean? This restriction may be\n"
, " lifted later."
]