zephyr-0.5.3: app/Command/Run.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}
-- | Dead code elimination command based on `Language.PureScript.CoreFn.DCE`.
--
module Command.Run
( runZephyr
) where
import Control.Applicative ((<|>))
import Control.Monad
import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Supply
import Control.Monad.Trans (lift)
import Control.Monad.Trans.Except
import Control.Exception
import Control.Concurrent.QSem
import qualified Control.Concurrent.Async as Async
import qualified Data.Aeson as A
import Data.Aeson.Internal (JSONPath)
import qualified Data.Aeson.Internal as A
import Data.Aeson.Parser (eitherDecodeWith, json)
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.UTF8 as BU8
import Data.Bool (bool)
import Data.Either (lefts, rights, partitionEithers)
import Data.Foldable (for_, traverse_)
import qualified Data.Map as M
import qualified Data.Set as S
import Data.Maybe (isNothing, listToMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as TE
import Data.Version (Version)
import Formatting (sformat, string, stext, (%))
import GHC.Conc.Sync (getNumCapabilities)
import qualified Language.PureScript.Docs.Types as Docs
import qualified Language.JavaScript.Parser as JS
import qualified Language.PureScript as P
import qualified Language.PureScript.CoreFn as CoreFn
import qualified Language.PureScript.CoreFn.FromJSON as CoreFn
import qualified Language.PureScript.Errors.JSON as P
import qualified System.Console.ANSI as ANSI
import System.Directory (copyFile, doesDirectoryExist, doesFileExist,
getCurrentDirectory, removeFile)
import System.Exit (exitFailure, exitSuccess)
import System.FilePath ((</>), (-<.>))
import System.FilePath.Glob (compile, globDir1)
import System.IO (hPutStrLn, stderr)
import Command.Options
import Language.PureScript.DCE.Errors (EntryPoint (..))
import Language.PureScript.DCE ( DCEError (..)
, Level (..)
)
import qualified Language.PureScript.DCE as DCE
readInput :: [FilePath]
-> IO [Either
(FilePath, JSONPath, String)
(Version, CoreFn.Module CoreFn.Ann)
]
readInput inputFiles = do
-- limit parallelizm to at most the number of capablities
sem <- getNumCapabilities >>= newQSem
threads <-
forM inputFiles $ \f -> do
waitQSem sem
mask $ \unmask -> Async.async $
(unmask $ do
c <- BSL.readFile f
-- being strict here forces reading the file and promptly closing its file
-- descriptor
case decodeCoreFn c of
Left (p, e) -> pure $ Left (f, p, e)
Right r@(!_, !_) -> pure $ Right r
)
`finally`
signalQSem sem
forM threads Async.wait
where
decodeCoreFn :: BSL.ByteString
-> Either (JSONPath, String)
(Version, CoreFn.Module CoreFn.Ann)
decodeCoreFn = eitherDecodeWith json (A.iparse CoreFn.moduleFromJSON)
-- | Argumnets: verbose, use JSON, warnings, errors
--
printWarningsAndErrors
:: Bool -- ^ be verbose
-> Bool -- ^ use 'JSON'
-> P.MultipleErrors -- ^ warnings
-> Either P.MultipleErrors a -- ^ errors
-> IO ()
printWarningsAndErrors verbose False warnings errors = do
pwd <- getCurrentDirectory
cc <- bool Nothing (Just P.defaultCodeColor) <$> ANSI.hSupportsANSI stderr
let ppeOpts = P.defaultPPEOptions { P.ppeCodeColor = cc, P.ppeFull = verbose, P.ppeRelativeDirectory = pwd }
when (P.nonEmpty warnings) $
hPutStrLn stderr (P.prettyPrintMultipleWarnings ppeOpts warnings)
case errors of
Left errs -> do
hPutStrLn stderr (P.prettyPrintMultipleErrors ppeOpts errs)
exitFailure
Right _ -> return ()
printWarningsAndErrors verbose True warnings errors = do
hPutStrLn stderr . BU8.toString . A.encode $
P.JSONResult (P.toJSONErrors verbose P.Warning [] warnings)
(either (P.toJSONErrors verbose P.Error []) (const []) errors)
either (const exitFailure) (const (return ())) errors
-- | Application exception
--
data DCEAppError
= ParseErrors ![Text]
-- ^ parser errors
| InputNotDirectory !FilePath
-- ^ input directory does not exists (or is not a directory)
| NoInputs !FilePath
-- ^ no input files
| DCEAppError !(DCEError 'Error)
-- ^ PureScript errors
-- | Render 'DCEAppError' as 'Text'
--
formatDCEAppError :: Options -> FilePath -> DCEAppError -> Text
formatDCEAppError opts _ (ParseErrors errs) =
let errs' =
if optVerbose opts
then errs
else take 5 errs ++ case length $ drop 5 errs of
0 -> []
x -> ["... (" <> T.pack (show x) <> " more)"]
in sformat
(string % ": Failed parsing:\n " % stext)
(DCE.colorString DCE.errorColor "Error")
(T.intercalate "\n\t" errs')
formatDCEAppError _ _ (NoInputs path)
= sformat
(stext % ": No inputs found under " % string % " directory.\n"
% " Please run `purs compile --codegen corefn ..` or"
% "`pulp build -- --codegen corefn`")
(DCE.colorText DCE.errorColor "Error")
(DCE.colorString DCE.codeColor path)
formatDCEAppError _ _ (InputNotDirectory path)
= sformat
(stext % ": Directory "%string%" does not exists.")
(DCE.colorText DCE.errorColor "Error")
(DCE.colorString DCE.codeColor path)
formatDCEAppError _ relPath (DCEAppError err)
= T.pack $ DCE.displayDCEError relPath err
-- | Given list of modules and list of entry points, find qualilfied names of
-- roots.
--
getEntryPoints
:: [CoreFn.Module CoreFn.Ann]
-> [EntryPoint]
-> [Either EntryPoint (P.Qualified P.Ident)]
getEntryPoints mods = go []
where
go acc [] = acc
go acc ((EntryPoint i) : eps) =
if i `fnd` mods
then go (Right i : acc) eps
else go (Left (EntryPoint i) : acc) eps
go acc ((EntryModule mn) : eps) = go (modExports mn mods ++ acc) eps
go acc ((err@EntryParseError{}) : eps) = go (Left err : acc) eps
modExports :: P.ModuleName -> [CoreFn.Module CoreFn.Ann] -> [Either EntryPoint (P.Qualified P.Ident)]
modExports mn [] = [Left (EntryModule mn)]
modExports mn (CoreFn.Module{ CoreFn.moduleName, CoreFn.moduleExports, CoreFn.moduleReExports } : ms)
| mn == moduleName
= (Right . flip P.mkQualified mn) `map` (moduleExports ++ (snd =<< M.toList moduleReExports))
| otherwise
= modExports mn ms
fnd :: P.Qualified P.Ident -> [CoreFn.Module CoreFn.Ann] -> Bool
fnd _ [] = False
fnd qi@(P.Qualified (P.ByModuleName mn) i) (pModule@CoreFn.Module{ CoreFn.moduleName } : ms)
= if moduleName == mn && i `elem` (allExports pModule)
then True
else fnd qi ms
fnd _ _ = False
allExports :: CoreFn.Module CoreFn.Ann -> [P.Ident]
allExports CoreFn.Module{ CoreFn.moduleExports, CoreFn.moduleReExports } =
moduleExports ++ (M.toList moduleReExports >>= snd)
dceCommand :: Options -> ExceptT DCEAppError IO ()
dceCommand Options { optEntryPoints
, optInputDir
, optOutputDir
, optVerbose
, optForeign
, optPureScriptOptions
, optUsePrefix
, optJsonErrors
, optEvaluate
} = do
-- initial checks
inptDirExist <- lift $ doesDirectoryExist optInputDir
unless inptDirExist $
throwError (InputNotDirectory optInputDir)
-- read files, parse errors
let cfnGlb = compile "**/corefn.json"
inpts0 <- liftIO $ globDir1 cfnGlb optInputDir >>= readInput
-- force inputs sequentially
inpts <- liftIO $ traverse evaluate inpts0
let errs = lefts inpts
unless (null errs) $
throwError (ParseErrors $ formatError `map` errs)
let mPursVer = fmap fst . listToMaybe . rights $ inpts
when (isNothing mPursVer) $
throwError (NoInputs optInputDir)
let (notFound, entryPoints) =
partitionEithers
(getEntryPoints
(fmap snd . rights $ inpts)
optEntryPoints)
when (not $ null notFound) $
case filter DCE.isEntryParseError notFound of
[] -> throwError (DCEAppError $ EntryPointsNotFound notFound)
perrs ->
let fn (EntryParseError s) acc = s : acc
fn _ acc = acc
in throwError (DCEAppError $ EntryPointsNotParsed (foldr fn [] perrs))
when (null entryPoints) $
throwError (DCEAppError NoEntryPoint)
-- run `evaluate` and `runDeadCodeElimination` on `CoreFn` representation
let mods = if optEvaluate
then DCE.runDeadCodeElimination
entryPoints
(DCE.evaluate (snd `map` rights inpts))
else DCE.runDeadCodeElimination
entryPoints
(snd `map` rights inpts)
let filePathMap =
M.fromList
(map
(\m -> (CoreFn.moduleName m, Right $ CoreFn.modulePath m))
mods)
foreigns <- P.inferForeignModules filePathMap
let makeActions = (P.buildMakeActions optOutputDir filePathMap foreigns optUsePrefix)
{ P.ffiCodegen = \CoreFn.Module{ CoreFn.moduleName, CoreFn.moduleForeign } -> liftIO $ do
let codegenTargets = P.optionsCodegenTargets optPureScriptOptions
when (S.member P.JS codegenTargets) $ do
case moduleName `M.lookup` foreigns of
-- run `runForeignModuleDeadCodeElimination`
Just path | optForeign -> do
jsCode <- BU8.toString <$> BSL.readFile path
case JS.parseModule jsCode path of
Left _ -> return ()
Right (JS.JSAstModule items ann) ->
let items' = DCE.runForeignModuleDeadCodeElimination moduleForeign items
jsAst' = JS.JSAstModule items' ann
foreignFile = optOutputDir
</> T.unpack (P.runModuleName moduleName)
</> "foreign.js"
in BSL.writeFile foreignFile (TE.encodeUtf8 $ JS.renderToText jsAst')
Right _ -> return ()
Just path -> do
let filePath = T.unpack (P.runModuleName moduleName)
outputPath = optInputDir </> filePath </> "foreign.js"
-- prefer foreign module in 'optOutputDir'.
bool path outputPath <$> doesFileExist outputPath
>>= (`copyFile` (optOutputDir </> filePath </> "foreign.js"))
Nothing -> pure ()
}
(makeErrors, makeWarnings) <-
liftIO
$ P.runMake optPureScriptOptions
$ runSupplyT 0
$ traverse
(\m ->
P.codegen makeActions m
(Docs.Module (CoreFn.moduleName m) Nothing [] [])
(moduleToExternsFile m))
mods
traverse_ (liftIO . P.runMake optPureScriptOptions . P.ffiCodegen makeActions) mods
-- copy extern files; We do not have access to data to regenerate extern
-- files (they relay on more information than is present in 'CoreFn.Module'
-- represenation).
for_ mods $ \m -> lift $ do
let mn = CoreFn.moduleName m
copyExterns mn "cbor" <|> do
-- zephyr will always generate "externs.cbor" file, if we are working
-- on a project using purescript-0.13.6 we need to remove it.
removeFile (optOutputDir </> (T.unpack $ P.runModuleName mn) </> "externs.cbor")
copyExterns mn "json"
liftIO $
printWarningsAndErrors
(P.optionsVerboseErrors optPureScriptOptions)
optJsonErrors
(suppressFFIErrors makeWarnings)
(either (Left . suppressFFIErrors) Right makeErrors)
where
formatError :: (FilePath, JSONPath, String) -> Text
formatError (f, p, err) =
if optVerbose
then sformat (string%":\n "%string) f (A.formatError p err)
else T.pack f
copyExterns :: P.ModuleName -> String -> IO ()
copyExterns mn extension = do
let filePath = T.unpack . P.runModuleName $ mn
copyFile (optInputDir </> filePath </> "externs" -<.> extension)
(optOutputDir </> filePath </> "externs" -<.> extension)
-- a hack: purescript codegen function reads FFI from disk, and checks
-- against it.
suppressFFIErrors :: P.MultipleErrors -> P.MultipleErrors
suppressFFIErrors (P.MultipleErrors errs) = P.MultipleErrors $ filter fn errs
where
fn (P.ErrorMessage _ P.UnnecessaryFFIModule{}) = False
fn (P.ErrorMessage _ P.UnusedFFIImplementations{}) = False
fn _ = True
moduleToExternsFile :: CoreFn.Module a -> P.ExternsFile
moduleToExternsFile CoreFn.Module {CoreFn.moduleName} = P.ExternsFile {
P.efVersion = mempty,
P.efModuleName = moduleName,
P.efExports = [],
P.efImports = [],
P.efFixities = [],
P.efTypeFixities = [],
P.efDeclarations = [],
P.efSourceSpan = P.SourceSpan "none" (P.SourcePos 0 0) (P.SourcePos 0 0)
}
runZephyr
:: Options
-> IO ()
runZephyr opts = do
res <- runExceptT $ dceCommand opts
relPath <- getCurrentDirectory
case res of
Left e ->
(hPutStrLn stderr . T.unpack . formatDCEAppError opts relPath $ e)
*> exitFailure
Right{} ->
exitSuccess