{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE CPP #-} -- | The Hell shell. module Hell.Shell (module Hell.Types ,module Data.Default ,startHell) where import Hell.Types import Control.Applicative import Control.Exception import Control.Monad.Reader import Control.Monad.Trans import Data.Default import Data.Dynamic import Data.IORef import Data.List import Data.Maybe import Data.Monoid import qualified Data.Text as T import DynFlags import Exception (ExceptionMonad) import GHC hiding (History) import GHC.Paths hiding (ghc) import Name import Outputable (Outputable(..),showSDoc) import System.Console.Haskeline import System.Console.Haskeline.History import System.Directory import System.FilePath import System.Posix.User -- | Go to hell. startHell :: Config -> IO () startHell unreadyConfig = do home <- io getHomeDirectory let config = unreadyConfig { configHistory = reifyHome home (configHistory unreadyConfig) } runGhc (Just libdir) (do dflags <- getSessionDynFlags void (setSessionDynFlags (setFlags [Opt_ImplicitPrelude] dflags)) setImports (configImports config) historyRef <- io (readHistory (configHistory config) >>= newIORef) username <- io getEffectiveUserName candidates <- fmap (map (occNameString . nameOccName)) getNamesInScope runReaderT (runHell repl) (HellState config historyRef username home candidates)) -- | Read-eval-print loop. repl :: Hell () repl = do state <- ask config <- asks stateConfig welcome <- asks (configWelcome . stateConfig) unless (null welcome) (haskeline (outputStrLn welcome)) loop config state -- | Do the get-line-and-looping. loop :: Config -> HellState -> Hell () loop config state = fix (\again -> do (mline,history) <- getLineAndHistory config state case mline of Nothing -> again Just line -> do historyRef <- asks stateHistory io (writeIORef historyRef history) _ <- ghc (runLine line) io (writeHistory (configHistory config) history) again) -- | Get a new line and return it with a new history. getLineAndHistory :: Config -> HellState -> Hell (Maybe String, History) getLineAndHistory config state = do pwd <- io getCurrentDirectory prompt <- prompter (stateUsername state) (stripHome home pwd) haskeline (do line <- getInputLine prompt history <- getHistory return (line,history)) where prompter = configPrompt config home = stateHome state -- | Transform ~/foo to /home/chris/foo. reifyHome :: FilePath -> String -> FilePath reifyHome home fp | isPrefixOf "~/" fp = home drop 2 fp | otherwise = fp -- | Strip and replace /home/chris/blah with ~/blah. stripHome :: FilePath -> FilePath -> FilePath stripHome home path | isPrefixOf home path = "~/" ++ dropWhile (=='/') (drop (length home) path) | otherwise = path -- | Import the given modules. setImports :: [String] -> Ghc () setImports = mapM (fmap IIDecl . parseImportDecl) >=> setContext -- | Compile the given expression and evaluate it. runLine :: String -> Ghc () runLine expr = do mtyp <- gtry (exprType expr) d <- getDynFlags case mtyp of Left err -> io (putStrLn (show err)) Right ty -> do let tyStr = showppr d ty if isPrefixOf "GHC.Types.IO " tyStr then runPrintableIO tyStr expr else if isInfixOf "Conduit" tyStr then runConduit tyStr expr else runExpr tyStr expr -- | Compile the given IO statement and run it as IO, printing the -- result. runConduit :: String -> String -> Ghc () runConduit typ expr = do result <- gcatch (fmap Right (dynCompileExpr e)) (\(e::SomeException) -> return (Left e)) case result of Left {} -> liftIO (putStrLn typ) Right compiled -> gcatch (io (fromDyn compiled (putStrLn "Bad compile."))) (\(e::SomeException) -> liftIO (print e)) where e = "Data.Conduit.Shell.run (" ++ expr ++ ") :: IO ()" -- | Compile the given IO statement and run it as IO, printing the -- result. runPrintableIO :: String -> String -> Ghc () runPrintableIO ty expr = do result <- gcatch (fmap Right (dynCompileExpr e)) (\(e::SomeException) -> return (Left e)) case result of Left {} -> runIO ty expr Right compiled -> gcatch (io (fromDyn compiled (putStrLn "Bad compile."))) (\(e::SomeException) -> liftIO (print e)) where e | ty == "GHC.Types.IO ()" = expr | otherwise = "(" ++ expr ++ ") >>= Prelude.print" -- | Compile the given IO statement and run it as IO. No result -- printed. runIO :: String -> String -> Ghc () runIO typ expr = do result <- gcatch (fmap Right (dynCompileExpr e)) (\(e::SomeException) -> return (Left e)) case result of Left {} -> liftIO (putStrLn typ) Right compiled -> gcatch (io (fromDyn compiled (putStrLn "Bad compile."))) (\(e::SomeException) -> liftIO (print e)) where e = "(" ++ expr ++ ") >> return ()" -- | Compile the given expression and print it. runExpr :: String -> String -> Ghc () runExpr ty expr = do result <- gcatch (fmap Right (dynCompileExpr e)) (\(e::SomeException) -> return (Left e)) case result of Left {} -> liftIO (putStrLn ty) Right compiled -> do liftIO (putStrLn ty) gcatch (io (fromDyn compiled (putStrLn "Bad compile."))) (\(e::SomeException) -> liftIO (print e)) where e = "Prelude.print (" ++ expr ++ ")" -- | Short-hand utility. io :: MonadIO m => IO a -> m a io = Control.Monad.Trans.liftIO -- | Run a Haskeline action in Hell. haskeline :: InputT IO a -> Hell a haskeline m = do historyRef <- asks stateHistory history <- io (readIORef historyRef) state <- ask io (runInputT (settings state) (do putHistory history m)) where settings state = setComplete (completeFilesAndFunctions (stateFunctions state)) defaultSettings -- | Complete file names or functions in scope. completeFilesAndFunctions :: [String] -> (String,String) -> IO (String,[Completion]) completeFilesAndFunctions funcs (leftReversed,right) = do (fileCandidate,fileResults) <- completeFilename (leftReversed,right) return (fileCandidate <|> funcCandidate,map speech fileResults <> funcResults) where speech (Completion (normalize -> rep) d fin) = Completion newrep d fin where newrep = (if isPrefixOf "\"" rep then rep else "\"" <> rep) <> "\"" funcResults = mapMaybe (completeFunc (reverse leftReversed)) funcs funcCandidate = "" normalize = T.unpack . T.replace "\\ " " " . T.pack -- | Complete a function name. completeFunc :: String -> String -> Maybe Completion completeFunc left func = if isPrefixOf left func then Just (Completion func func True) else Nothing -- | Run a GHC action in Hell. ghc :: Ghc a -> Hell a ghc m = Hell (ReaderT (const m)) -- | Set the given flags. setFlags :: [ExtensionFlag] -> DynFlags -> DynFlags setFlags xs dflags = foldl xopt_set dflags xs -- | Something like Show but for things which annoyingly do not have -- Show but Outputable instead. showppr :: Outputable a => DynFlags -> a -> String showppr d = showSDoc d . ppr -- | Try the thing or return the exception. gtry :: (Functor m, ExceptionMonad m) => m a -> m (Either SomeException a) gtry m = gcatch (fmap Right m) (\(e::SomeException) -> return (Left e))