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
31 -- * Implementation of Make API using files on disk
37import Control.Applicative
38import Control.Arrow ((&&&))
40import Control.Monad.Error.Class (MonadError(..))
41import Control.Monad.Trans.Except
42import Control.Monad.Reader
43import Control.Monad.Writer
44import Control.Monad.Supply
46import Data.Function (on)
47import Data.List (sortBy, groupBy)
48import Data.Maybe (fromMaybe)
50import Data.Traversable (traverse)
51import Data.Version (showVersion)
52import qualified Data.Map as M
53import qualified Data.Set as S
55import System.Directory
56 (doesFileExist, getModificationTime, createDirectoryIfMissing)
57import System.FilePath ((</>), takeDirectory)
58import System.IO.Error (tryIOError)
61import Language.PureScript.AST
62import Language.PureScript.CodeGen.Externs (moduleToPs)
63import Language.PureScript.Environment
64import Language.PureScript.Errors
65import Language.PureScript.Linter
66import Language.PureScript.ModuleDependencies
67import Language.PureScript.Names
68import Language.PureScript.Options
69import Language.PureScript.Parser
70import Language.PureScript.Pretty
71import Language.PureScript.Renamer
72import Language.PureScript.Sugar
73import Language.PureScript.TypeChecker
74import qualified Language.PureScript.Constants as C
76import qualified Language.PureScript.CodeGen.JS as J
77import qualified Language.PureScript.CoreFn as CF
78import qualified Paths_purescript as Paths
81-- Actions that require implementations when running in "make" mode.
83data MakeActions m = MakeActions {
85 -- Get the timestamp for the input file(s) for a module. If there are multiple
86 -- files (.purs and foreign files, for example) the timestamp should be for
87 -- the most recently modified file.
89 getInputTimestamp :: ModuleName -> m (Either RebuildPolicy (Maybe UTCTime))
91 -- Get the timestamp for the output files for a module. This should be the
92 -- timestamp for the oldest modified file, or Nothing if any of the required
93 -- output files are missing.
95 , getOutputTimestamp :: ModuleName -> m (Maybe UTCTime)
97 -- Read the externs file for a module as a string and also return the actual
99 , readExterns :: ModuleName -> m (FilePath, String)
101 -- Run the code generator for the module and write any required output files.
103 , codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT m ()
105 -- Respond to a progress update.
107 , progress :: String -> m ()
111-- Generated code for an externs file.
116-- Determines when to rebuild a module
119 -- | Never rebuild this module
121 -- | Always rebuild this module
122 | RebuildAlways deriving (Show, Eq, Ord)
125-- Compiles in "make" mode, compiling each module separately to a js files and an externs file
127-- If timestamps have not changed, the externs file can be used to provide the module's types without
128-- having to typecheck the module again.
130make :: forall m. (Functor m, Applicative m, Monad m, MonadReader Options m, MonadError MultipleErrors m, MonadWriter MultipleErrors m)
134make MakeActions{..} ms = do
135 (sorted, graph) <- sortModules $ map importPrim ms
137 toRebuild <- foldM (\s (Module _ moduleName' _ _) -> do
138 inputTimestamp <- getInputTimestamp moduleName'
139 outputTimestamp <- getOutputTimestamp moduleName'
140 return $ case (inputTimestamp, outputTimestamp) of
141 (Right (Just t1), Just t2) | t1 < t2 -> s
142 (Left RebuildNever, Just _) -> s
143 _ -> S.insert moduleName' s) S.empty sorted
145 marked <- rebuildIfNecessary (reverseDependencies graph) toRebuild sorted
146 (desugared, nextVar) <- runSupplyT 0 $ zip (map fst marked) <$> desugar (map snd marked)
147 evalSupplyT nextVar $ go initEnvironment desugared
150 go :: Environment -> [(Bool, Module)] -> SupplyT m Environment
151 go env [] = return env
152 go env ((False, m) : ms') = do
153 (_, env') <- lift . runCheck' env $ typeCheckModule Nothing m
155 go env ((True, m@(Module coms moduleName' _ exps)) : ms') = do
156 lift $ progress $ "Compiling " ++ runModuleName moduleName'
157 (checked@(Module _ _ elaborated _), env') <- lift . runCheck' env $ typeCheckModule Nothing m
158 checkExhaustiveModule env' checked
159 regrouped <- createBindingGroups moduleName' . collapseBindingGroups $ elaborated
160 let mod' = Module coms moduleName' regrouped exps
161 corefn = CF.moduleToCoreFn env' mod'
162 [renamed] = renameInModules [corefn]
163 exts = moduleToPs mod' env'
164 codegen renamed env' exts
167 rebuildIfNecessary :: M.Map ModuleName [ModuleName] -> S.Set ModuleName -> [Module] -> m [(Bool, Module)]
168 rebuildIfNecessary _ _ [] = return []
169 rebuildIfNecessary graph toRebuild (m@(Module _ moduleName' _ _) : ms') | moduleName' `S.member` toRebuild = do
170 let deps = fromMaybe [] $ moduleName' `M.lookup` graph
171 toRebuild' = toRebuild `S.union` S.fromList deps
172 (:) (True, m) <$> rebuildIfNecessary graph toRebuild' ms'
173 rebuildIfNecessary graph toRebuild (Module _ moduleName' _ _ : ms') = do
174 (path, externs) <- readExterns moduleName'
175 externsModules <- fmap (map snd) . alterErrors $ parseModulesFromFiles id [(path, externs)]
176 case externsModules of
177 [m'@(Module _ moduleName'' _ _)] | moduleName'' == moduleName' -> (:) (False, m') <$> rebuildIfNecessary graph toRebuild ms'
178 _ -> throwError . errorMessage . InvalidExternsFile $ path
180 alterErrors = flip catchError $ \(MultipleErrors errs) ->
181 throwError . MultipleErrors $ flip map errs $ \e -> case e of
182 SimpleErrorWrapper (ErrorParsingModule err) -> SimpleErrorWrapper (ErrorParsingExterns err)
185reverseDependencies :: ModuleGraph -> M.Map ModuleName [ModuleName]
186reverseDependencies g = combine [ (dep, mn) | (mn, deps) <- g, dep <- deps ]
188 combine :: (Ord a) => [(a, b)] -> M.Map a [b]
189 combine = M.fromList . map ((fst . head) &&& map snd) . groupBy ((==) `on` fst) . sortBy (compare `on` fst)
192-- Add an import declaration for a module if it does not already explicitly import it.
194addDefaultImport :: ModuleName -> Module -> Module
195addDefaultImport toImport m@(Module coms mn decls exps) =
196 if isExistingImport `any` decls || mn == toImport then m
197 else Module coms mn (ImportDeclaration toImport Implicit Nothing : decls) exps
199 isExistingImport (ImportDeclaration mn' _ _) | mn' == toImport = True
200 isExistingImport (PositionedDeclaration _ _ d) = isExistingImport d
201 isExistingImport _ = False
203importPrim :: Module -> Module
204importPrim = addDefaultImport (ModuleName [ProperName C.prim])
207-- A monad for running make actions
209newtype Make a = Make { unMake :: ReaderT Options (WriterT MultipleErrors (ExceptT MultipleErrors IO)) a }
210 deriving (Functor, Applicative, Monad, MonadIO, MonadError MultipleErrors, MonadWriter MultipleErrors, MonadReader Options)
213-- Execute a 'Make' monad, returning either errors, or the result of the compile plus any warnings.
215runMake :: Options -> Make a -> IO (Either MultipleErrors (a, MultipleErrors))
216runMake opts = runExceptT . runWriterT . flip runReaderT opts . unMake
218makeIO :: (IOError -> ErrorMessage) -> IO a -> Make a
220 e <- liftIO $ tryIOError io
221 either (throwError . singleError . f) return e
223-- Traverse (Either e) instance (base 4.7)
224traverseEither :: Applicative f => (a -> f b) -> Either e a -> f (Either e b)
225traverseEither _ (Left x) = pure (Left x)
226traverseEither f (Right y) = Right <$> f y
229-- A set of make actions that read and write modules from the given directory.
231buildMakeActions :: FilePath -- ^ the output directory
232 -> M.Map ModuleName (Either RebuildPolicy String) -- ^ a map between module names and paths to the file containing the PureScript module
233 -> M.Map ModuleName (FilePath, ForeignJS) -- ^ a map between module name and the file containing the foreign javascript for the module
234 -> Bool -- ^ Generate a prefix comment?
236buildMakeActions outputDir filePathMap foreigns usePrefix =
237 MakeActions getInputTimestamp getOutputTimestamp readExterns codegen progress
240 getInputTimestamp :: ModuleName -> Make (Either RebuildPolicy (Maybe UTCTime))
241 getInputTimestamp mn = do
242 let path = fromMaybe (error "Module has no filename in 'make'") $ M.lookup mn filePathMap
243 e1 <- traverseEither getTimestamp path
244 fPath <- maybe (return Nothing) (getTimestamp . fst) $ M.lookup mn foreigns
245 return $ fmap (max fPath) e1
247 getOutputTimestamp :: ModuleName -> Make (Maybe UTCTime)
248 getOutputTimestamp mn = do
249 let filePath = runModuleName mn
250 jsFile = outputDir </> filePath </> "index.js"
251 externsFile = outputDir </> filePath </> "externs.purs"
252 min <$> getTimestamp jsFile <*> getTimestamp externsFile
254 readExterns :: ModuleName -> Make (FilePath, String)
256 let path = outputDir </> runModuleName mn </> "externs.purs"
257 (path, ) <$> readTextFile path
259 codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT Make ()
260 codegen m _ exts = do
261 let mn = CF.moduleName m
262 foreignInclude <- case mn `M.lookup` foreigns of
264 | not $ requiresForeign m -> do
265 tell $ errorMessage $ UnnecessaryFFIModule mn path
267 | otherwise -> return $ Just $ J.JSApp (J.JSVar "require") [J.JSStringLiteral "./foreign"]
268 Nothing | requiresForeign m -> throwError . errorMessage $ MissingFFIModule mn
269 | otherwise -> return Nothing
270 pjs <- prettyPrintJS <$> J.moduleToJs m foreignInclude
271 let filePath = runModuleName mn
272 jsFile = outputDir </> filePath </> "index.js"
273 externsFile = outputDir </> filePath </> "externs.purs"
274 foreignFile = outputDir </> filePath </> "foreign.js"
275 prefix = ["Generated by psc version " ++ showVersion Paths.version | usePrefix]
276 js = unlines $ map ("// " ++) prefix ++ [pjs]
277 verboseErrorsEnabled <- asks optionsVerboseErrors
279 when verboseErrorsEnabled $ progress $ "Writing " ++ jsFile
280 writeTextFile jsFile js
281 maybe (return ()) (writeTextFile foreignFile . snd) $ mn `M.lookup` foreigns
282 writeTextFile externsFile exts
284 requiresForeign :: CF.Module a -> Bool
285 requiresForeign = not . null . CF.moduleForeign
287 getTimestamp :: FilePath -> Make (Maybe UTCTime)
288 getTimestamp path = makeIO (const (SimpleErrorWrapper $ CannotGetFileInfo path)) $ do
289 exists <- doesFileExist path
290 traverse (const $ getModificationTime path) $ guard exists
292 readTextFile :: FilePath -> Make String
293 readTextFile path = makeIO (const (SimpleErrorWrapper $ CannotReadFile path)) $ readFile path
295 writeTextFile :: FilePath -> String -> Make ()
296 writeTextFile path text = makeIO (const (SimpleErrorWrapper $ CannotWriteFile path)) $ do
300 mkdirp :: FilePath -> IO ()
301 mkdirp = createDirectoryIfMissing True . takeDirectory
303 progress :: String -> Make ()
304 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