zephyr-0.1.4: app/Command/DCE.hs
-- | Dead code elimination command based on `Language.PureScript.CoreFn.DCE`.
module Command.DCE
( runDCECommand
, dceOptions
, entryPointOpt
) where
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.Monad.Writer
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 Data.Bifunctor (first)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as B (fromStrict, toStrict)
import qualified Data.ByteString.UTF8 as BU8
import Data.Bool (bool)
import Data.Either (Either, lefts, rights)
import Data.Foldable (traverse_)
import Data.List (intercalate, null)
import qualified Data.Map as M
import Data.Maybe (isNothing, listToMaybe)
import Data.Monoid ((<>))
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Data.Traversable (for)
import Data.Version (Version)
import Formatting (sformat, string, stext, (%))
import qualified Language.PureScript as P
import qualified Language.PureScript.CoreFn as CoreFn
import qualified Language.PureScript.CoreFn.FromJSON as CoreFn
import Language.PureScript.DCE
import qualified Language.PureScript.Errors.JSON as P
import qualified Options.Applicative as Opts
import qualified System.Console.ANSI as ANSI
import System.Directory (doesDirectoryExist, getCurrentDirectory)
import System.Exit (exitFailure, exitSuccess)
import System.FilePath.Glob (compile, globDir1)
import System.IO (hPutStrLn, stderr)
import Command.DCEOptions
inputDirectoryOpt :: Opts.Parser FilePath
inputDirectoryOpt = Opts.strOption $
Opts.short 'i'
<> Opts.long "input-directory"
<> Opts.value "output"
<> Opts.showDefault
<> Opts.help "Input directory (purs output directory)."
outputDirectoryOpt :: Opts.Parser FilePath
outputDirectoryOpt = Opts.strOption $
Opts.short 'o'
<> Opts.long "dce-output"
<> Opts.value "dce-output"
<> Opts.showDefault
<> Opts.help "Output directory."
entryPointOpt :: Opts.Parser EntryPoint
entryPointOpt = Opts.argument (Opts.auto >>= checkIfQualified) $
Opts.metavar "entry-point"
<> Opts.help "Qualified identifier. All code which is not a transitive dependency of an entry point will be removed. You can pass multiple entry points."
where
checkIfQualified (EntryPoint q@(P.Qualified Nothing _)) = fail $
"not a qualified indentifier: '" ++ T.unpack (P.showQualified P.runIdent q) ++ "'"
checkIfQualified e = return e
verboseOutputOpt :: Opts.Parser Bool
verboseOutputOpt = Opts.switch $
Opts.short 'v'
<> Opts.long "verbose"
<> Opts.showDefault
<> Opts.help "Verbose CoreFn parser errors."
dceForeignOpt :: Opts.Parser Bool
dceForeignOpt = Opts.switch $
Opts.short 'f'
<> Opts.long "dce-foreign"
<> Opts.showDefault
<> Opts.help "dce foriegn modules"
comments :: Opts.Parser Bool
comments = Opts.switch $
Opts.short 'c'
<> Opts.long "comments"
<> Opts.help "Include comments in the generated code"
verboseErrors :: Opts.Parser Bool
verboseErrors = Opts.switch $
Opts.short 'v'
<> Opts.long "verbose-errors"
<> Opts.help "Display verbose error messages"
codegenTargets :: Opts.Parser [P.CodegenTarget]
codegenTargets = Opts.option targetParser $
Opts.short 'g'
<> Opts.long "codegen"
<> Opts.value [P.JS]
<> Opts.help
( "Specifies comma-separated codegen targets to include. "
<> targetsMessage
<> " The default target is 'js', but if this option is used only the targets specified will be used."
)
targets :: M.Map String P.CodegenTarget
targets = M.fromList
[ ("js", P.JS)
, ("sourcemaps", P.JSSourceMap)
, ("corefn", P.CoreFn)
]
targetsMessage :: String
targetsMessage = "Accepted codegen targets are '" <> intercalate "', '" (M.keys targets) <> "'."
targetParser :: Opts.ReadM [P.CodegenTarget]
targetParser =
Opts.str >>= \s ->
for (T.split (== ',') s)
$ maybe (Opts.readerError targetsMessage) pure
. flip M.lookup targets
. T.unpack
. T.strip
noPrefix :: Opts.Parser Bool
noPrefix = Opts.switch $
Opts.short 'p'
<> Opts.long "no-prefix"
<> Opts.help "Do not include comment header"
jsonErrors :: Opts.Parser Bool
jsonErrors = Opts.switch $
Opts.long "json-errors"
<> Opts.help "Print errors to stderr as JSON"
pureScriptOptions :: Opts.Parser P.Options
pureScriptOptions =
P.Options
<$> verboseErrors
<*> (not <$> comments)
<*> (handleTargets <$> codegenTargets)
where
-- Ensure that the JS target is included if sourcemaps are
handleTargets :: [P.CodegenTarget] -> S.Set P.CodegenTarget
handleTargets ts = S.fromList (if P.JSSourceMap `elem` ts then P.JS : ts else ts)
dceOptions :: Opts.Parser DCEOptions
dceOptions = DCEOptions
<$> Opts.many entryPointOpt
<*> inputDirectoryOpt
<*> outputDirectoryOpt
<*> verboseOutputOpt
<*> dceForeignOpt
<*> pureScriptOptions
<*> (not <$> noPrefix)
<*> jsonErrors
readInput :: [FilePath] -> IO [Either (FilePath, JSONPath, String) (Version, CoreFn.Module CoreFn.Ann)]
readInput inputFiles = forM inputFiles (\f -> addPath f . decodeCoreFn <$> B.readFile f)
where
decodeCoreFn :: B.ByteString -> Either (JSONPath, String) (Version, CoreFn.Module CoreFn.Ann)
decodeCoreFn = eitherDecodeWith json (A.iparse CoreFn.moduleFromJSON) . B.fromStrict
addPath
:: FilePath
-> Either (JSONPath, String) (Version, CoreFn.Module CoreFn.Ann)
-> Either (FilePath, JSONPath, String) (Version, CoreFn.Module CoreFn.Ann)
addPath f = either (Left . incl) Right
where incl (l,r) = (f,l,r)
--
-- | Argumnets: verbose, use JSON, warnings, errors
printWarningsAndErrors :: Bool -> Bool -> P.MultipleErrors -> Either P.MultipleErrors a -> 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 . B.toStrict . A.encode $
P.JSONResult (P.toJSONErrors verbose P.Warning warnings)
(either (P.toJSONErrors verbose P.Error) (const []) errors)
either (const exitFailure) (const (return ())) errors
data DCEAppError
= ParseErrors [Text]
| InputNotDirectory FilePath
| NoInputs FilePath
| CompilationError (DCEError 'Error)
formatDCEAppError :: DCEOptions -> FilePath -> DCEAppError -> Text
formatDCEAppError opts _ (ParseErrors errs) =
let errs' =
if dceVerbose 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)
(colorString 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`")
(colorText errorColor "Error")
(colorString codeColor path)
formatDCEAppError _ _ (InputNotDirectory path)
= sformat
(stext%": Directory "%string%" does not exists.")
(colorText errorColor "Error")
(colorString codeColor path)
formatDCEAppError _ relPath (CompilationError err)
= T.pack $ displayDCEError relPath err
dceCommand :: DCEOptions -> ExceptT DCEAppError IO ()
dceCommand DCEOptions {..} = do
-- initial checks
inptDirExist <- lift $ doesDirectoryExist dceInputDir
unless inptDirExist $
throwError (InputNotDirectory dceInputDir)
-- read files, parse errors
let entryPoints = runEntryPoint <$> dceEntryPoints
cfnGlb = compile "**/corefn.json"
inpts <- liftIO $ globDir1 cfnGlb dceInputDir >>= readInput
let errs = lefts inpts
unless (null errs) $
throwError (ParseErrors $ formatError `map` errs)
let mPursVer = fmap fst . listToMaybe . rights $ inpts
when (isNothing mPursVer) $
throwError (NoInputs dceInputDir)
-- run `dceEval` and `dce` on the `CoreFn`
(mods, warns) <- mapExceptT (fmap $ first CompilationError)
$ runWriterT
$ dceEval (snd `map` rights inpts) >>= flip dce entryPoints
relPath <- liftIO getCurrentDirectory
liftIO $ traverse_ (hPutStrLn stderr . uncurry (displayDCEWarning relPath)) (zip (zip [1..] (repeat (length warns))) warns)
let filePathMap = M.fromList $ map (\m -> (CoreFn.moduleName m, Right $ CoreFn.modulePath m)) mods
foreigns <- P.inferForeignModules filePathMap
let makeActions = P.buildMakeActions dceOutputDir filePathMap foreigns dceUsePrefix
(makeErrors, makeWarnings) <-
liftIO
$ P.runMake dcePureScriptOptions
$ runSupplyT 0 $ traverse (\m -> P.codegen makeActions m P.initEnvironment mempty) mods
liftIO $ printWarningsAndErrors (P.optionsVerboseErrors dcePureScriptOptions) dceJsonErrors makeWarnings makeErrors
return ()
where
formatError :: (FilePath, JSONPath, String) -> Text
formatError (f, p, err) =
if dceVerbose
then sformat (string%":\n "%string) f (A.formatError p err)
else T.pack f
runDCECommand
:: DCEOptions
-> IO ()
runDCECommand opts = do
res <- runExceptT $ dceCommand opts
relPath <- getCurrentDirectory
case res of
Left e ->
(hPutStrLn stderr . T.unpack . formatDCEAppError opts relPath $ e)
*> exitFailure
Right{} ->
exitSuccess