1-----------------------------------------------------------------------------
4-- Copyright : (c) 2013-14 Phil Freeman, (c) 2014 Gary Burgess, and other contributors
7-- Maintainer : Phil Freeman <paf31@cantab.net>
8-- Stability : experimental
13-----------------------------------------------------------------------------
15{-# LANGUAGE DataKinds #-}
16{-# LANGUAGE FlexibleContexts #-}
17{-# LANGUAGE GeneralizedNewtypeDeriving #-}
18{-# LANGUAGE TupleSections #-}
19{-# LANGUAGE RankNTypes #-}
20{-# LANGUAGE RecordWildCards #-}
21{-# LANGUAGE ScopedTypeVariables #-}
23module Language.PureScript.Make
32 -- * Implementation of Make API using files on disk
38import Control.Applicative
39import Control.Arrow ((&&&))
41import Control.Monad.Error.Class (MonadError(..))
42import Control.Monad.Trans.Except
43import Control.Monad.Reader
44import Control.Monad.Writer
45import Control.Monad.Supply
46import Control.Monad.Supply.Class (fresh)
48import Data.Function (on)
49import Data.List (sortBy, groupBy)
50import Data.Maybe (fromMaybe)
52import Data.Traversable (traverse)
53import Data.Version (showVersion)
54import qualified Data.Map as M
55import qualified Data.Set as S
57import System.Directory
58 (doesFileExist, getModificationTime, createDirectoryIfMissing)
59import System.FilePath ((</>), takeDirectory)
60import System.IO.Error (tryIOError)
63import Language.PureScript.AST
64import Language.PureScript.CodeGen.Externs (moduleToPs)
65import Language.PureScript.Environment
66import Language.PureScript.Errors
67import Language.PureScript.Linter
68import Language.PureScript.ModuleDependencies
69import Language.PureScript.Names
70import Language.PureScript.Options
71import Language.PureScript.Parser
72import Language.PureScript.Pretty
73import Language.PureScript.Renamer
74import Language.PureScript.Sugar
75import Language.PureScript.TypeChecker
76import qualified Language.PureScript.Constants as C
78import qualified Language.PureScript.CodeGen.JS as J
79import qualified Language.PureScript.CoreFn as CF
80import qualified Paths_purescript as Paths
83-- Actions that require implementations when running in "make" mode.
85data MakeActions m = MakeActions {
87 -- Get the timestamp for the input file(s) for a module. If there are multiple
88 -- files (.purs and foreign files, for example) the timestamp should be for
89 -- the most recently modified file.
91 getInputTimestamp :: ModuleName -> m (Either RebuildPolicy (Maybe UTCTime))
93 -- Get the timestamp for the output files for a module. This should be the
94 -- timestamp for the oldest modified file, or Nothing if any of the required
95 -- output files are missing.
97 , getOutputTimestamp :: ModuleName -> m (Maybe UTCTime)
99 -- Read the externs file for a module as a string and also return the actual
100 -- path for the file.
101 , readExterns :: ModuleName -> m (FilePath, String)
103 -- Run the code generator for the module and write any required output files.
105 , codegen :: CF.Module CF.Ann -> Environment -> SupplyVar -> Externs -> m ()
107 -- Respond to a progress update.
109 , progress :: String -> m ()
113-- Generated code for an externs file.
118-- A value to be used in the Supply monad.
120type SupplyVar = Integer
123-- Determines when to rebuild a module
126 -- | Never rebuild this module
128 -- | Always rebuild this module
129 | RebuildAlways deriving (Show, Eq, Ord)
132-- Compiles in "make" mode, compiling each module separately to a js files and an externs file
134-- If timestamps have not changed, the externs file can be used to provide the module's types without
135-- having to typecheck the module again.
137make :: forall m. (Functor m, Applicative m, Monad m, MonadReader Options m, MonadError MultipleErrors m, MonadWriter MultipleErrors m)
139 -> [(Either RebuildPolicy FilePath, Module)]
141make MakeActions{..} ms = do
142 (sorted, graph) <- sortModules $ map (importPrim . snd) ms
144 toRebuild <- foldM (\s (Module _ moduleName' _ _) -> do
145 inputTimestamp <- getInputTimestamp moduleName'
146 outputTimestamp <- getOutputTimestamp moduleName'
147 return $ case (inputTimestamp, outputTimestamp) of
148 (Right (Just t1), Just t2) | t1 < t2 -> s
149 (Left RebuildNever, Just _) -> s
150 _ -> S.insert moduleName' s) S.empty sorted
152 marked <- rebuildIfNecessary (reverseDependencies graph) toRebuild sorted
153 (desugared, nextVar) <- runSupplyT 0 $ zip (map fst marked) <$> desugar (map snd marked)
154 evalSupplyT nextVar $ go initEnvironment desugared
157 go :: Environment -> [(Bool, Module)] -> SupplyT m Environment
158 go env [] = return env
159 go env ((False, m) : ms') = do
160 (_, env') <- lift . runCheck' env $ typeCheckModule Nothing m
162 go env ((True, m@(Module coms moduleName' _ exps)) : ms') = do
163 lift $ progress $ "Compiling " ++ runModuleName moduleName'
164 (Module _ _ elaborated _, env') <- lift . runCheck' env $ typeCheckModule Nothing m
165 regrouped <- createBindingGroups moduleName' . collapseBindingGroups $ elaborated
166 let mod' = Module coms moduleName' regrouped exps
167 corefn = CF.moduleToCoreFn env' mod'
168 [renamed] = renameInModules [corefn]
169 exts = moduleToPs mod' env'
171 lift $ codegen renamed env' nextVar exts
174 rebuildIfNecessary :: M.Map ModuleName [ModuleName] -> S.Set ModuleName -> [Module] -> m [(Bool, Module)]
175 rebuildIfNecessary _ _ [] = return []
176 rebuildIfNecessary graph toRebuild (m@(Module _ moduleName' _ _) : ms') | moduleName' `S.member` toRebuild = do
177 let deps = fromMaybe [] $ moduleName' `M.lookup` graph
178 toRebuild' = toRebuild `S.union` S.fromList deps
179 (:) (True, m) <$> rebuildIfNecessary graph toRebuild' ms'
180 rebuildIfNecessary graph toRebuild (Module _ moduleName' _ _ : ms') = do
181 (path, externs) <- readExterns moduleName'
182 externsModules <- fmap (map snd) . alterErrors $ parseModulesFromFiles id [(path, externs)]
183 case externsModules of
184 [m'@(Module _ moduleName'' _ _)] | moduleName'' == moduleName' -> (:) (False, m') <$> rebuildIfNecessary graph toRebuild ms'
185 _ -> throwError . errorMessage . InvalidExternsFile $ path
187 alterErrors = flip catchError $ \(MultipleErrors errs) ->
188 throwError . MultipleErrors $ flip map errs $ \e -> case e of
189 SimpleErrorWrapper (ErrorParsingModule err) -> SimpleErrorWrapper (ErrorParsingExterns err)
192reverseDependencies :: ModuleGraph -> M.Map ModuleName [ModuleName]
193reverseDependencies g = combine [ (dep, mn) | (mn, deps) <- g, dep <- deps ]
195 combine :: (Ord a) => [(a, b)] -> M.Map a [b]
196 combine = M.fromList . map ((fst . head) &&& map snd) . groupBy ((==) `on` fst) . sortBy (compare `on` fst)
199-- Add an import declaration for a module if it does not already explicitly import it.
201addDefaultImport :: ModuleName -> Module -> Module
202addDefaultImport toImport m@(Module coms mn decls exps) =
203 if isExistingImport `any` decls || mn == toImport then m
204 else Module coms mn (ImportDeclaration toImport Implicit Nothing : decls) exps
206 isExistingImport (ImportDeclaration mn' _ _) | mn' == toImport = True
207 isExistingImport (PositionedDeclaration _ _ d) = isExistingImport d
208 isExistingImport _ = False
210importPrim :: Module -> Module
211importPrim = addDefaultImport (ModuleName [ProperName C.prim])
214-- A monad for running make actions
216newtype Make a = Make { unMake :: ReaderT Options (WriterT MultipleErrors (ExceptT MultipleErrors IO)) a }
217 deriving (Functor, Applicative, Monad, MonadIO, MonadError MultipleErrors, MonadWriter MultipleErrors, MonadReader Options)
220-- Execute a 'Make' monad, returning either errors, or the result of the compile plus any warnings.
222runMake :: Options -> Make a -> IO (Either MultipleErrors (a, MultipleErrors))
223runMake opts = runExceptT . runWriterT . flip runReaderT opts . unMake
225makeIO :: (IOError -> ErrorMessage) -> IO a -> Make a
227 e <- liftIO $ tryIOError io
228 either (throwError . singleError . f) return e
230-- Traverse (Either e) instance (base 4.7)
231traverseEither :: Applicative f => (a -> f b) -> Either e a -> f (Either e b)
232traverseEither _ (Left x) = pure (Left x)
233traverseEither f (Right y) = Right <$> f y
236-- A set of make actions that read and write modules from the given directory.
238buildMakeActions :: FilePath -- ^ the output directory
239 -> M.Map ModuleName (Either RebuildPolicy String) -- ^ a map between module names and paths to the file containing the PureScript module
240 -> M.Map ModuleName (FilePath, ForeignJS) -- ^ a map between module name and the file containing the foreign javascript for the module
241 -> Bool -- ^ Generate a prefix comment?
243buildMakeActions outputDir filePathMap foreigns usePrefix =
244 MakeActions getInputTimestamp getOutputTimestamp readExterns codegen progress
247 getInputTimestamp :: ModuleName -> Make (Either RebuildPolicy (Maybe UTCTime))
248 getInputTimestamp mn = do
249 let path = fromMaybe (error "Module has no filename in 'make'") $ M.lookup mn filePathMap
250 e1 <- traverseEither getTimestamp path
251 fPath <- maybe (return Nothing) (getTimestamp . fst) $ M.lookup mn foreigns
252 return $ fmap (max fPath) e1
254 getOutputTimestamp :: ModuleName -> Make (Maybe UTCTime)
255 getOutputTimestamp mn = do
256 let filePath = runModuleName mn
257 jsFile = outputDir </> filePath </> "index.js"
258 externsFile = outputDir </> filePath </> "externs.purs"
259 min <$> getTimestamp jsFile <*> getTimestamp externsFile
261 readExterns :: ModuleName -> Make (FilePath, String)
263 let path = outputDir </> runModuleName mn </> "externs.purs"
264 (path, ) <$> readTextFile path
266 codegen :: CF.Module CF.Ann -> Environment -> SupplyVar -> Externs -> Make ()
267 codegen m _ nextVar exts = do
268 let mn = CF.moduleName m
269 foreignInclude <- case mn `M.lookup` foreigns of
271 | not $ requiresForeign m -> do
272 tell $ errorMessage $ UnnecessaryFFIModule mn path
274 | otherwise -> return $ Just $ J.JSApp (J.JSVar "require") [J.JSStringLiteral "./foreign"]
275 Nothing | requiresForeign m -> throwError . errorMessage $ MissingFFIModule mn
276 | otherwise -> return Nothing
277 pjs <- evalSupplyT nextVar $ prettyPrintJS <$> J.moduleToJs m foreignInclude
278 let filePath = runModuleName mn
279 jsFile = outputDir </> filePath </> "index.js"
280 externsFile = outputDir </> filePath </> "externs.purs"
281 foreignFile = outputDir </> filePath </> "foreign.js"
282 prefix = ["Generated by psc version " ++ showVersion Paths.version | usePrefix]
283 js = unlines $ map ("// " ++) prefix ++ [pjs]
284 writeTextFile jsFile js
285 maybe (return ()) (writeTextFile foreignFile . snd) $ mn `M.lookup` foreigns
286 writeTextFile externsFile exts
288 requiresForeign :: CF.Module a -> Bool
289 requiresForeign = not . null . CF.moduleForeign
291 getTimestamp :: FilePath -> Make (Maybe UTCTime)
292 getTimestamp path = makeIO (const (SimpleErrorWrapper $ CannotGetFileInfo path)) $ do
293 exists <- doesFileExist path
294 traverse (const $ getModificationTime path) $ guard exists
296 readTextFile :: FilePath -> Make String
297 readTextFile path = do
298 verboseErrorsEnabled <- asks optionsVerboseErrors
299 makeIO (const (SimpleErrorWrapper $ CannotReadFile path)) $ do
300 when verboseErrorsEnabled $ putStrLn $ "Reading " ++ path
303 writeTextFile :: FilePath -> String -> Make ()
304 writeTextFile path text = makeIO (const (SimpleErrorWrapper $ CannotWriteFile path)) $ do
306 putStrLn $ "Writing " ++ path
309 mkdirp :: FilePath -> IO ()
310 mkdirp = createDirectoryIfMissing True . takeDirectory
312 progress :: String -> Make ()
313 progress = liftIO . putStrLn
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list