1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3{-# LANGUAGE TupleSections #-}
4{-# LANGUAGE RankNTypes #-}
5{-# LANGUAGE RecordWildCards #-}
6{-# LANGUAGE ScopedTypeVariables #-}
7{-# LANGUAGE MultiParamTypeClasses #-}
8{-# LANGUAGE TypeFamilies #-}
10module Language.PureScript.Make
14 , ProgressMessage(..), renderProgressMessage
20 -- * Implementation of Make API using files on disk
29import Control.Applicative ((<|>))
30import Control.Monad hiding (sequence)
31import Control.Monad.Error.Class (MonadError(..))
32import Control.Monad.Writer.Class (MonadWriter(..))
33import Control.Monad.Trans.Class (MonadTrans(..))
34import Control.Monad.Trans.Except
35import Control.Monad.IO.Class
36import Control.Monad.Reader (MonadReader(..), ReaderT(..), asks)
37import Control.Monad.Logger
38import Control.Monad.Supply
39import Control.Monad.Base (MonadBase(..))
40import Control.Monad.Trans.Control (MonadBaseControl(..))
42import Control.Concurrent.Lifted as C
44import Data.List (foldl', sort)
45import Data.Maybe (fromMaybe, catMaybes, isJust)
46import Data.Either (partitionEithers)
48import Data.String (fromString)
49import Data.Foldable (for_)
50import Data.Traversable (for)
51import Data.Version (showVersion)
52import Data.Aeson (encode, decode)
53import qualified Data.ByteString.Lazy as B
54import qualified Data.ByteString.UTF8 as BU8
55import qualified Data.Set as S
56import qualified Data.Map as M
58import qualified Text.Parsec as Parsec
63import System.Directory
64 (doesFileExist, getModificationTime, createDirectoryIfMissing, getCurrentDirectory)
65import System.FilePath ((</>), takeDirectory, makeRelative, splitPath, normalise)
66import System.IO.Error (tryIOError)
67import System.IO.UTF8 (readUTF8File, writeUTF8File)
69import qualified Language.JavaScript.Parser as JS
71import Language.PureScript.Crash
72import Language.PureScript.AST
73import Language.PureScript.Externs
74import Language.PureScript.Environment
75import Language.PureScript.Errors
76import Language.PureScript.Linter
77import Language.PureScript.ModuleDependencies
78import Language.PureScript.Names
79import Language.PureScript.Options
80import Language.PureScript.Pretty
81import Language.PureScript.Pretty.Common(SMap(..))
82import Language.PureScript.Renamer
83import Language.PureScript.Sugar
84import Language.PureScript.TypeChecker
85import qualified Language.PureScript.Constants as C
86import qualified Language.PureScript.Bundle as Bundle
87import qualified Language.PureScript.Parser as PSParser
89import qualified Language.PureScript.CodeGen.JS as J
90import qualified Language.PureScript.CoreFn as CF
91import qualified Paths_purescript as Paths
93-- | Progress messages from the make process
95 = CompilingModule ModuleName
96 deriving (Show, Read, Eq, Ord)
98-- | Render a progress message
99renderProgressMessage :: ProgressMessage -> String
100renderProgressMessage (CompilingModule mn) = "Compiling " ++ runModuleName mn
102-- | Actions that require implementations when running in "make" mode.
104-- This type exists to make two things abstract:
106-- * The particular backend being used (Javascript, C++11, etc.)
108-- * The details of how files are read/written etc.
110data MakeActions m = MakeActions {
112 -- Get the timestamp for the input file(s) for a module. If there are multiple
113 -- files (.purs and foreign files, for example) the timestamp should be for
114 -- the most recently modified file.
116 getInputTimestamp :: ModuleName -> m (Either RebuildPolicy (Maybe UTCTime))
118 -- Get the timestamp for the output files for a module. This should be the
119 -- timestamp for the oldest modified file, or Nothing if any of the required
120 -- output files are missing.
122 , getOutputTimestamp :: ModuleName -> m (Maybe UTCTime)
124 -- Read the externs file for a module as a string and also return the actual
125 -- path for the file.
126 , readExterns :: ModuleName -> m (FilePath, Externs)
128 -- Run the code generator for the module and write any required output files.
130 , codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT m ()
132 -- Respond to a progress update.
134 , progress :: ProgressMessage -> m ()
138-- Generated code for an externs file.
143-- Determines when to rebuild a module
146 -- | Never rebuild this module
148 -- | Always rebuild this module
149 | RebuildAlways deriving (Show, Read, Eq, Ord)
151-- | Rebuild a single module
152rebuildModule :: forall m. (Monad m, MonadBaseControl IO m, MonadReader Options m, MonadError MultipleErrors m, MonadWriter MultipleErrors m)
157rebuildModule MakeActions{..} externs m@(Module _ _ moduleName _ _) = do
158 progress $ CompilingModule moduleName
159 let env = foldl' (flip applyExternsFileToEnvironment) initEnvironment externs
161 ((checked@(Module ss coms _ elaborated exps), env'), nextVar) <- runSupplyT 0 $ do
162 [desugared] <- desugar externs [m]
163 runCheck' env $ typeCheckModule desugared
164 checkExhaustiveModule env' checked
165 regrouped <- createBindingGroups moduleName . collapseBindingGroups $ elaborated
166 let mod' = Module ss coms moduleName regrouped exps
167 corefn = CF.moduleToCoreFn env' mod'
168 [renamed] = renameInModules [corefn]
169 exts = moduleToExternsFile mod' env'
170 evalSupplyT nextVar . codegen renamed env' . BU8.toString . B.toStrict . encode $ exts
174-- Compiles in "make" mode, compiling each module separately to a js files and an externs file
176-- If timestamps have not changed, the externs file can be used to provide the module's types without
177-- having to typecheck the module again.
179make :: forall m. (Monad m, MonadBaseControl IO m, MonadReader Options m, MonadError MultipleErrors m, MonadWriter MultipleErrors m)
183make ma@MakeActions{..} ms = do
184 requirePath <- asks optionsRequirePath
185 when (isJust requirePath) $ tell $ errorMessage DeprecatedRequirePath
187 checkModuleNamesAreUnique
189 (sorted, graph) <- sortModules ms
191 barriers <- zip (map getModuleName sorted) <$> replicateM (length ms) ((,) <$> C.newEmptyMVar <*> C.newEmptyMVar)
193 for_ sorted $ \m -> fork $ do
194 let deps = fromMaybe (internalError "make: module not found in dependency graph.") (lookup (getModuleName m) graph)
195 buildModule barriers (importPrim m) (deps `inOrderOf` map getModuleName sorted)
197 -- Wait for all threads to complete, and collect errors.
198 errors <- catMaybes <$> for barriers (takeMVar . snd . snd)
200 -- All threads have completed, rethrow any caught errors.
201 unless (null errors) $ throwError (mconcat errors)
203 -- Bundle up all the externs and return them as an Environment
204 (_, externs) <- unzip . fromMaybe (internalError "make: externs were missing but no errors reported.") . sequence <$> for barriers (takeMVar . fst . snd)
205 return $ foldl' (flip applyExternsFileToEnvironment) initEnvironment externs
208 checkModuleNamesAreUnique :: m ()
209 checkModuleNamesAreUnique =
210 case findDuplicate (map getModuleName ms) of
212 Just mn -> throwError . errorMessage $ DuplicateModuleName mn
214 -- Verify that a list of values has unique keys
215 findDuplicate :: (Ord a) => [a] -> Maybe a
216 findDuplicate = go . sort
220 | otherwise = go (y : xs)
223 -- Sort a list so its elements appear in the same order as in another list.
224 inOrderOf :: (Ord a) => [a] -> [a] -> [a]
225 inOrderOf xs ys = let s = S.fromList xs in filter (`S.member` s) ys
227 buildModule :: [(ModuleName, (C.MVar (Maybe (MultipleErrors, ExternsFile)), C.MVar (Maybe MultipleErrors)))] -> Module -> [ModuleName] -> m ()
228 buildModule barriers m@(Module _ _ moduleName _ _) deps = flip catchError (markComplete Nothing . Just) $ do
229 -- We need to wait for dependencies to be built, before checking if the current
230 -- module should be rebuilt, so the first thing to do is to wait on the
231 -- MVars for the module's dependencies.
232 mexterns <- fmap unzip . sequence <$> traverse (readMVar . fst . fromMaybe (internalError "make: no barrier") . flip lookup barriers) deps
235 Just (_, externs) -> do
236 outputTimestamp <- getOutputTimestamp moduleName
237 dependencyTimestamp <- maximumMaybe <$> traverse (fmap shouldExist . getOutputTimestamp) deps
238 inputTimestamp <- getInputTimestamp moduleName
240 let shouldRebuild = case (inputTimestamp, dependencyTimestamp, outputTimestamp) of
241 (Right (Just t1), Just t3, Just t2) -> t1 > t2 || t3 > t2
242 (Right (Just t1), Nothing, Just t2) -> t1 > t2
243 (Left RebuildNever, _, Just _) -> False
247 (exts, warnings) <- listen $ rebuildModule ma externs m
248 markComplete (Just (warnings, exts)) Nothing
253 mexts <- decodeExterns . snd <$> readExterns moduleName
255 Just exts -> markComplete (Just (mempty, exts)) Nothing
257 Nothing -> markComplete Nothing Nothing
259 markComplete :: Maybe (MultipleErrors, ExternsFile) -> Maybe MultipleErrors -> m ()
260 markComplete externs errors = do
261 putMVar (fst $ fromMaybe (internalError "make: no barrier") $ lookup moduleName barriers) externs
262 putMVar (snd $ fromMaybe (internalError "make: no barrier") $ lookup moduleName barriers) errors
264 maximumMaybe :: (Ord a) => [a] -> Maybe a
265 maximumMaybe [] = Nothing
266 maximumMaybe xs = Just $ maximum xs
268 -- Make sure a dependency exists
269 shouldExist :: Maybe UTCTime -> UTCTime
270 shouldExist (Just t) = t
271 shouldExist _ = internalError "make: dependency should already have been built."
273 decodeExterns :: Externs -> Maybe ExternsFile
274 decodeExterns bs = do
275 externs <- decode (fromString bs)
276 guard $ efVersion externs == showVersion Paths.version
279importPrim :: Module -> Module
280importPrim = addDefaultImport (ModuleName [ProperName C.prim])
283-- A monad for running make actions
285newtype Make a = Make { unMake :: ReaderT Options (ExceptT MultipleErrors (Logger MultipleErrors)) a }
286 deriving (Functor, Applicative, Monad, MonadIO, MonadError MultipleErrors, MonadWriter MultipleErrors, MonadReader Options)
288instance MonadBase IO Make where
291instance MonadBaseControl IO Make where
292 type StM Make a = Either MultipleErrors a
293 liftBaseWith f = Make $ liftBaseWith $ \q -> f (q . unMake)
294 restoreM = Make . restoreM
297-- Execute a 'Make' monad, returning either errors, or the result of the compile plus any warnings.
299runMake :: Options -> Make a -> IO (Either MultipleErrors a, MultipleErrors)
300runMake opts = runLogger' . runExceptT . flip runReaderT opts . unMake
302makeIO :: (IOError -> ErrorMessage) -> IO a -> Make a
304 e <- liftIO $ tryIOError io
305 either (throwError . singleError . f) return e
307-- Traverse (Either e) instance (base 4.7)
308traverseEither :: Applicative f => (a -> f b) -> Either e a -> f (Either e b)
309traverseEither _ (Left x) = pure (Left x)
310traverseEither f (Right y) = Right <$> f y
313-- A set of make actions that read and write modules from the given directory.
315buildMakeActions :: FilePath -- ^ the output directory
316 -> M.Map ModuleName (Either RebuildPolicy FilePath) -- ^ a map between module names and paths to the file containing the PureScript module
317 -> M.Map ModuleName FilePath -- ^ a map between module name and the file containing the foreign javascript for the module
318 -> Bool -- ^ Generate a prefix comment?
320buildMakeActions outputDir filePathMap foreigns usePrefix =
321 MakeActions getInputTimestamp getOutputTimestamp readExterns codegen progress
324 getInputTimestamp :: ModuleName -> Make (Either RebuildPolicy (Maybe UTCTime))
325 getInputTimestamp mn = do
326 let path = fromMaybe (internalError "Module has no filename in 'make'") $ M.lookup mn filePathMap
327 e1 <- traverseEither getTimestamp path
328 fPath <- maybe (return Nothing) getTimestamp $ M.lookup mn foreigns
329 return $ fmap (max fPath) e1
331 getOutputTimestamp :: ModuleName -> Make (Maybe UTCTime)
332 getOutputTimestamp mn = do
333 let filePath = runModuleName mn
334 jsFile = outputDir </> filePath </> "index.js"
335 externsFile = outputDir </> filePath </> "externs.json"
336 min <$> getTimestamp jsFile <*> getTimestamp externsFile
338 readExterns :: ModuleName -> Make (FilePath, Externs)
340 let path = outputDir </> runModuleName mn </> "externs.json"
341 (path, ) <$> readTextFile path
343 codegen :: CF.Module CF.Ann -> Environment -> Externs -> SupplyT Make ()
344 codegen m _ exts = do
345 let mn = CF.moduleName m
346 foreignInclude <- case mn `M.lookup` foreigns of
348 | not $ requiresForeign m -> do
349 tell $ errorMessage $ UnnecessaryFFIModule mn path
352 checkForeignDecls m path
353 return $ Just $ J.JSApp Nothing (J.JSVar Nothing "require") [J.JSStringLiteral Nothing "./foreign"]
354 Nothing | requiresForeign m -> throwError . errorMessage $ MissingFFIModule mn
355 | otherwise -> return Nothing
356 rawJs <- J.moduleToJs m foreignInclude
357 dir <- lift $ makeIO (const (ErrorMessage [] $ CannotGetFileInfo ".")) getCurrentDirectory
358 sourceMaps <- lift $ asks optionsSourceMaps
359 let (pjs, mappings) = if sourceMaps then prettyPrintJSWithSourceMaps rawJs else (prettyPrintJS rawJs, [])
360 let filePath = runModuleName mn
361 jsFile = outputDir </> filePath </> "index.js"
362 mapFile = outputDir </> filePath </> "index.js.map"
363 externsFile = outputDir </> filePath </> "externs.json"
364 foreignFile = outputDir </> filePath </> "foreign.js"
365 prefix = ["Generated by psc version " ++ showVersion Paths.version | usePrefix]
366 js = unlines $ map ("// " ++) prefix ++ [pjs]
367 mapRef = if sourceMaps then "//# sourceMappingURL=index.js.map\n" else ""
369 writeTextFile jsFile (fromString $ js ++ mapRef)
370 for_ (mn `M.lookup` foreigns) (readTextFile >=> writeTextFile foreignFile)
371 writeTextFile externsFile exts
372 lift $ when sourceMaps $ genSourceMap dir mapFile (length prefix) mappings
374 genSourceMap :: String -> String -> Int -> [SMap] -> Make ()
375 genSourceMap dir mapFile extraLines mappings = do
376 let pathToDir = iterate (".." </>) ".." !! length (splitPath $ normalise outputDir)
377 sourceFile = case mappings of
378 ((SMap file _ _):_) -> Just $ pathToDir </> makeRelative dir file
380 let rawMapping = SourceMapping { smFile = "index.js", smSourceRoot = Nothing, smMappings =
381 map (\(SMap _ orig gen) -> Mapping {
382 mapOriginal = Just $ convertPos $ add 0 (-1) orig
383 , mapSourceFile = sourceFile
384 , mapGenerated = convertPos $ add (extraLines+1) 0 gen
388 let mapping = generate rawMapping
389 writeTextFile mapFile $ BU8.toString . B.toStrict . encode $ mapping
391 add :: Int -> Int -> SourcePos -> SourcePos
392 add n m (SourcePos n' m') = SourcePos (n+n') (m+m')
394 convertPos :: SourcePos -> Pos
395 convertPos SourcePos { sourcePosLine = l, sourcePosColumn = c } =
396 Pos { posLine = fromIntegral l, posColumn = fromIntegral c }
398 requiresForeign :: CF.Module a -> Bool
399 requiresForeign = not . null . CF.moduleForeign
401 getTimestamp :: FilePath -> Make (Maybe UTCTime)
402 getTimestamp path = makeIO (const (ErrorMessage [] $ CannotGetFileInfo path)) $ do
403 exists <- doesFileExist path
404 traverse (const $ getModificationTime path) $ guard exists
406 writeTextFile :: FilePath -> String -> Make ()
407 writeTextFile path text = makeIO (const (ErrorMessage [] $ CannotWriteFile path)) $ do
409 writeUTF8File path text
411 mkdirp :: FilePath -> IO ()
412 mkdirp = createDirectoryIfMissing True . takeDirectory
414 progress :: ProgressMessage -> Make ()
415 progress = liftIO . putStrLn . renderProgressMessage
417readTextFile :: FilePath -> Make String
418readTextFile path = makeIO (const (ErrorMessage [] $ CannotReadFile path)) $ readUTF8File path
421-- Check that the declarations in a given PureScript module match with those
422-- in its corresponding foreign module.
424checkForeignDecls :: CF.Module ann -> FilePath -> SupplyT Make ()
425checkForeignDecls m path = do
426 jsStr <- lift $ readTextFile path
427 js <- either (errorParsingModule . Bundle.UnableToParseModule) pure $ JS.parse jsStr path
429 foreignIdentsStrs <- either errorParsingModule pure $ getExps js
430 foreignIdents <- either
431 errorInvalidForeignIdentifiers
433 (parseIdents foreignIdentsStrs)
434 let importedIdents = S.fromList $ map fst (CF.moduleForeign m)
436 let unusedFFI = foreignIdents S.\\ importedIdents
437 unless (null unusedFFI) $
438 tell . errorMessage . UnusedFFIImplementations mname $
441 let missingFFI = importedIdents S.\\ foreignIdents
442 unless (null missingFFI) $
443 throwError . errorMessage . MissingFFIImplementations mname $
447 mname = CF.moduleName m
449 errorParsingModule :: Bundle.ErrorMessage -> SupplyT Make a
450 errorParsingModule = throwError . errorMessage . ErrorParsingFFIModule path . Just
452 getExps :: JS.JSAST -> Either Bundle.ErrorMessage [String]
453 getExps = Bundle.getExportedIdentifiers (runModuleName mname)
455 errorInvalidForeignIdentifiers :: [String] -> SupplyT Make a
456 errorInvalidForeignIdentifiers =
457 throwError . mconcat . map (errorMessage . InvalidFFIIdentifier mname)
459 parseIdents :: [String] -> Either [String] [Ident]
461 case partitionEithers (map parseIdent strs) of
467 -- TODO: Handling for parenthesised operators should be removed after 0.9.
468 -- We ignore the error message here, just being told it's an invalid
469 -- identifier should be enough.
470 parseIdent :: String -> Either String Ident
471 parseIdent str = try str <|> try ("(" ++ str ++ ")")
473 try s = either (const (Left str)) Right $ do
474 ts <- PSParser.lex "" s
475 PSParser.runTokenParser "" (PSParser.parseIdent <* Parsec.eof) ts
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