{-# LANGUAGE NoOverloadedStrings, NoImplicitPrelude, TypeSynonymInstances, GADTs, CPP #-} {- | Description : Wrapper around GHC API, exposing a single `evaluate` interface that runs a statement, declaration, import, or directive. This module exports all functions used for evaluation of IHaskell input. -} module IHaskell.Eval.Evaluate ( interpret, testInterpret, testEvaluate, evaluate, flushWidgetMessages, Interpreter, liftIO, typeCleaner, formatType, capturedIO, ) where import IHaskellPrelude import Control.Concurrent (forkIO, threadDelay) import Data.Foldable (foldMap) import Prelude (head, tail, last, init) import Data.List (nubBy) import qualified Data.Set as Set import Data.Char as Char import Data.Dynamic import qualified Data.Serialize as Serialize import qualified Debugger import System.Directory import System.Posix.IO (fdToHandle) import System.IO (hGetChar, hSetEncoding, utf8) import System.Random (getStdGen, randomRs) import System.Process import System.Exit import Data.Maybe (mapMaybe) import System.Environment (getEnv) import qualified GHC.Paths import InteractiveEval import DynFlags import Exception (gtry) import HscTypes import GhcMonad (liftIO) import GHC hiding (Stmt, TypeSig) import Exception hiding (evaluate) import Outputable hiding ((<>)) import Packages import Bag import qualified ErrUtils import IHaskell.Types import IHaskell.IPython import IHaskell.Eval.Parser import IHaskell.Eval.Lint import IHaskell.Display import qualified IHaskell.Eval.Hoogle as Hoogle import IHaskell.Eval.Util import IHaskell.BrokenPackages import StringUtils (replace, split, strip, rstrip) #if MIN_VERSION_ghc(8,2,0) import FastString (unpackFS) #else import Paths_ihaskell (version) import Data.Version (versionBranch) #endif -- | Set GHC's verbosity for debugging ghcVerbosity :: Maybe Int ghcVerbosity = Nothing -- Just 5 ignoreTypePrefixes :: [String] ignoreTypePrefixes = [ "GHC.Types" , "GHC.Base" , "GHC.Show" , "System.IO" , "GHC.Float" , ":Interactive" , "GHC.Num" , "GHC.IO" , "GHC.Integer.Type" ] typeCleaner :: String -> String typeCleaner = useStringType . foldl' (.) id (map (`replace` "") fullPrefixes) where fullPrefixes = map (++ ".") ignoreTypePrefixes useStringType = replace "[Char]" "String" -- MonadIO constraint necessary for GHC 7.6 write :: (MonadIO m, GhcMonad m) => KernelState -> String -> m () write state x = when (kernelDebug state) $ liftIO $ hPutStrLn stderr $ "DEBUG: " ++ x type Interpreter = Ghc requiredGlobalImports :: [String] requiredGlobalImports = [ "import qualified Prelude as IHaskellPrelude" , "import qualified System.Directory as IHaskellDirectory" , "import qualified System.Posix.IO as IHaskellIO" , "import qualified System.IO as IHaskellSysIO" , "import qualified Language.Haskell.TH as IHaskellTH" ] ihaskellGlobalImports :: [String] ihaskellGlobalImports = [ "import IHaskell.Display()" , "import qualified IHaskell.Display" , "import qualified IHaskell.IPython.Stdin" , "import qualified IHaskell.Eval.Widgets" ] hiddenPackageNames :: Set.Set String hiddenPackageNames = Set.fromList ["ghc-lib", "ghc-lib-parser"] -- | Interpreting function for testing. testInterpret :: Interpreter a -> IO a testInterpret v = interpret GHC.Paths.libdir False False (const v) -- | Evaluation function for testing. testEvaluate :: String -> IO () testEvaluate str = void $ testInterpret $ evaluate defaultKernelState str (\_ _ -> return ()) (\state _ -> return state) -- | Run an interpreting action. This is effectively runGhc with initialization -- and importing. The `allowedStdin` argument indicates whether `stdin` is -- handled specially, which cannot be done in a testing environment. The -- `needsSupportLibraries` argument indicates whether we want support libraries -- to be imported, which is not the case during testing. The argument passed to -- the action indicates whether the IHaskell library is available. interpret :: String -> Bool -> Bool -> (Bool -> Interpreter a) -> IO a interpret libdir allowedStdin needsSupportLibraries action = runGhc (Just libdir) $ do -- If we're in a sandbox, add the relevant package database sandboxPackages <- liftIO getSandboxPackageConf initGhci sandboxPackages case ghcVerbosity of Just verb -> do dflags <- getSessionDynFlags void $ setSessionDynFlags $ dflags { verbosity = verb } Nothing -> return () hasSupportLibraries <- initializeImports needsSupportLibraries -- Close stdin so it can't be used. Otherwise it'll block the kernel forever. dir <- liftIO getIHaskellDir let cmd = printf "IHaskell.IPython.Stdin.fixStdin \"%s\"" dir when (allowedStdin && hasSupportLibraries) $ void $ execStmt cmd execOptions initializeItVariable -- Run the rest of the interpreter action hasSupportLibraries packageIdString' :: DynFlags -> PackageConfig -> String packageIdString' dflags pkg_cfg = #if MIN_VERSION_ghc(8,2,0) case (lookupPackage dflags $ packageConfigId pkg_cfg) of Nothing -> "(unknown)" Just cfg -> let PackageName name = packageName cfg in unpackFS name #else fromMaybe "(unknown)" (unitIdPackageIdString dflags $ packageConfigId pkg_cfg) #endif getPackageConfigs :: DynFlags -> [PackageConfig] getPackageConfigs dflags = foldMap snd pkgDb where Just pkgDb = pkgDatabase dflags -- | Initialize our GHC session with imports and a value for 'it'. Return whether the IHaskell -- library is available. initializeImports :: Bool -> Interpreter Bool initializeImports importSupportLibraries = do -- Load packages that start with ihaskell-*, aren't just IHaskell, and depend directly on the right -- version of the ihaskell library. Also verify that the packages we load are not broken. dflags <- getSessionDynFlags broken <- liftIO getBrokenPackages (dflgs, _) <- liftIO $ initPackages dflags let db = getPackageConfigs dflgs packageNames = map (packageIdString' dflgs) db hiddenPackages = Set.intersection hiddenPackageNames (Set.fromList packageNames) hiddenFlags = fmap HidePackage $ Set.toList hiddenPackages initStr = "ihaskell-" #if MIN_VERSION_ghc(8,2,0) -- Name of the ihaskell package, i.e. "ihaskell" iHaskellPkgName = "ihaskell" #else -- Name of the ihaskell package, e.g. "ihaskell-1.2.3.4" iHaskellPkgName = initStr ++ intercalate "." (map show (versionBranch version)) #endif displayPkgs = [ pkgName | pkgName <- packageNames , Just (x:_) <- [stripPrefix initStr pkgName] , pkgName `notElem` broken , isAlpha x ] hasIHaskellPackage = not $ null $ filter (== iHaskellPkgName) packageNames -- Generate import statements all Display modules. let capitalize :: String -> String capitalize [] = [] capitalize (first:rest) = Char.toUpper first : rest importFmt = "import IHaskell.Display.%s" #if MIN_VERSION_ghc(8,2,0) toImportStmt :: String -> String toImportStmt = printf importFmt . concatMap capitalize . drop 1 . split "-" #else dropFirstAndLast :: [a] -> [a] dropFirstAndLast = reverse . drop 1 . reverse . drop 1 toImportStmt :: String -> String toImportStmt = printf importFmt . concatMap capitalize . dropFirstAndLast . split "-" #endif displayImports = map toImportStmt displayPkgs void $ setSessionDynFlags $ dflgs { packageFlags = hiddenFlags ++ packageFlags dflgs } -- Import implicit prelude. importDecl <- parseImportDecl "import Prelude" let implicitPrelude = importDecl { ideclImplicit = True } displayImports' = if importSupportLibraries then displayImports else [] -- Import modules. imports <- mapM parseImportDecl $ requiredGlobalImports ++ if hasIHaskellPackage then ihaskellGlobalImports ++ displayImports' else [] setContext $ map IIDecl $ implicitPrelude : imports return hasIHaskellPackage -- | Give a value for the `it` variable. initializeItVariable :: Interpreter () initializeItVariable = -- This is required due to the way we handle `it` in the wrapper statements - if it doesn't exist, -- the first statement will fail. void $ execStmt "let it = ()" execOptions -- | Publisher for IHaskell outputs. The first argument indicates whether this output is final -- (true) or intermediate (false). The second argument indicates whether the evaluation -- completed successfully (Success) or an error occurred (Failure). type Publisher = (EvaluationResult -> ErrorOccurred -> IO ()) -- | Output of a command evaluation. data EvalOut = EvalOut { evalStatus :: ErrorOccurred , evalResult :: Display , evalState :: KernelState , evalPager :: [DisplayData] , evalMsgs :: [WidgetMsg] } cleanString :: String -> String cleanString istr = if allBrackets then clean else istr where str = strip istr l = lines str allBrackets = all (fAny [isPrefixOf ">", null]) l fAny fs x = any ($ x) fs clean = unlines $ map removeBracket l removeBracket ('>':xs) = xs removeBracket [] = [] -- should never happen: removeBracket other = error $ "Expected bracket as first char, but got string: " ++ other -- | Evaluate some IPython input code. evaluate :: KernelState -- ^ The kernel state. -> String -- ^ Haskell code or other interpreter commands. -> Publisher -- ^ Function used to publish data outputs. -> (KernelState -> [WidgetMsg] -> IO KernelState) -- ^ Function to handle widget messages -> Interpreter KernelState evaluate kernelState code output widgetHandler = do cmds <- parseString (cleanString code) let execCount = getExecutionCounter kernelState -- Extract all parse errors. let justError x@ParseError{} = Just x justError _ = Nothing errs = mapMaybe (justError . unloc) cmds updated <- case errs of -- Only run things if there are no parse errors. [] -> do when (getLintStatus kernelState /= LintOff) $ liftIO $ do lintSuggestions <- lint code cmds unless (noResults lintSuggestions) $ output (FinalResult lintSuggestions [] []) Success runUntilFailure kernelState (map unloc cmds ++ [storeItCommand execCount]) -- Print all parse errors. _ -> do forM_ errs $ \err -> do out <- evalCommand output err kernelState liftIO $ output (FinalResult (evalResult out) [] []) (evalStatus out) return kernelState return updated { getExecutionCounter = execCount + 1 } where noResults (Display res) = null res noResults (ManyDisplay res) = all noResults res runUntilFailure :: KernelState -> [CodeBlock] -> Interpreter KernelState runUntilFailure state [] = return state runUntilFailure state (cmd:rest) = do evalOut <- evalCommand output cmd state -- Get displayed channel outputs. Merge them with normal display outputs. dispsMay <- if supportLibrariesAvailable state then do getEncodedDisplays <- extractValue "IHaskell.Display.displayFromChanEncoded" case getEncodedDisplays of Left err -> error $ "Deserialization error (Evaluate.hs): " ++ err Right displaysIO -> do result <- liftIO displaysIO case Serialize.decode result of Left err -> error $ "Deserialization error (Evaluate.hs): " ++ err Right res -> return res else return Nothing let result = case dispsMay of Nothing -> evalResult evalOut Just disps -> evalResult evalOut <> disps -- Output things only if they are non-empty. unless (noResults result && null (evalPager evalOut)) $ liftIO $ output (FinalResult result (evalPager evalOut) []) (evalStatus evalOut) let tempMsgs = evalMsgs evalOut tempState = evalState evalOut { evalMsgs = [] } -- Handle the widget messages newState <- if supportLibrariesAvailable state then flushWidgetMessages tempState tempMsgs widgetHandler else return tempState case evalStatus evalOut of Success -> runUntilFailure newState rest Failure -> return newState storeItCommand execCount = Statement $ printf "let it%d = it" execCount -- | Compile a string and extract a value from it. Effectively extract the result of an expression -- from inside the notebook environment. extractValue :: Typeable a => String -> Interpreter (Either String a) extractValue expr = do compiled <- dynCompileExpr expr case fromDynamic compiled of Nothing -> return (Left multipleIHaskells) Just result -> return (Right result) where multipleIHaskells = concat [ "The installed IHaskell support libraries do not match" , " the instance of IHaskell you are running.\n" , "This *may* cause problems with functioning of widgets or rich media displays.\n" , "This is most often caused by multiple copies of IHaskell" , " being installed simultaneously in your environment.\n" , "To resolve this issue, clear out your environment and reinstall IHaskell.\n" , "If you are installing support libraries, make sure you only do so once:\n" , " # Run this without first running `stack install ihaskell`\n" , " stack install ihaskell-diagrams\n" , "If you continue to have problems, please file an issue on Github." ] flushWidgetMessages :: KernelState -> [WidgetMsg] -> (KernelState -> [WidgetMsg] -> IO KernelState) -> Interpreter KernelState flushWidgetMessages state evalmsgs widgetHandler = do -- Capture all widget messages queued during code execution extracted <- extractValue "IHaskell.Eval.Widgets.relayWidgetMessages" liftIO $ case extracted of Left err -> do hPutStrLn stderr "Disabling IHaskell widget support due to an encountered error:" hPutStrLn stderr err return state Right messagesIO -> do messages <- messagesIO -- Handle all the widget messages let commMessages = evalmsgs ++ messages widgetHandler state commMessages getErrMsgDoc :: ErrUtils.ErrMsg -> SDoc getErrMsgDoc = ErrUtils.pprLocErrMsg safely :: KernelState -> Interpreter EvalOut -> Interpreter EvalOut safely state = ghandle handler . ghandle sourceErrorHandler where handler :: SomeException -> Interpreter EvalOut handler exception = return EvalOut { evalStatus = Failure , evalResult = displayError $ show exception , evalState = state , evalPager = [] , evalMsgs = [] } sourceErrorHandler :: SourceError -> Interpreter EvalOut sourceErrorHandler srcerr = do let msgs = bagToList $ srcErrorMessages srcerr errStrs <- forM msgs $ doc . getErrMsgDoc let fullErr = unlines errStrs return EvalOut { evalStatus = Failure , evalResult = displayError fullErr , evalState = state , evalPager = [] , evalMsgs = [] } wrapExecution :: KernelState -> Interpreter Display -> Interpreter EvalOut wrapExecution state exec = safely state $ exec >>= \res -> return EvalOut { evalStatus = Success , evalResult = res , evalState = state , evalPager = [] , evalMsgs = [] } -- | Return the display data for this command, as well as whether it resulted in an error. evalCommand :: Publisher -> CodeBlock -> KernelState -> Interpreter EvalOut evalCommand _ (Import importStr) state = wrapExecution state $ do write state $ "Import: " ++ importStr evalImport importStr return mempty evalCommand _ (Module contents) state = wrapExecution state $ do write state $ "Module:\n" ++ contents -- Write the module contents to a temporary file in our work directory namePieces <- getModuleName contents let directory = "./" ++ intercalate "/" (init namePieces) ++ "/" filename = last namePieces ++ ".hs" liftIO $ do createDirectoryIfMissing True directory writeFile (directory ++ filename) contents -- Clear old modules of this name let modName = intercalate "." namePieces removeTarget $ TargetModule $ mkModuleName modName removeTarget $ TargetFile filename Nothing -- Remember which modules we've loaded before. importedModules <- getContext let -- Get the dot-delimited pieces of the module name. moduleNameOf :: InteractiveImport -> [String] moduleNameOf (IIDecl decl) = split "." . moduleNameString . unLoc . ideclName $ decl moduleNameOf (IIModule imp) = split "." . moduleNameString $ imp -- Return whether this module prevents the loading of the one we're trying to load. If a module B -- exist, we cannot load A.B. All modules must have unique last names (where A.B has last name B). -- However, we *can* just reload a module. preventsLoading md = let pieces = moduleNameOf md in last namePieces == last pieces && namePieces /= pieces -- If we've loaded anything with the same last name, we can't use this. Otherwise, GHC tries to load -- the original *.hs fails and then fails. case find preventsLoading importedModules of -- If something prevents loading this module, return an error. Just previous -> do let prevLoaded = intercalate "." (moduleNameOf previous) return $ displayError $ printf "Can't load module %s because already loaded %s" modName prevLoaded -- Since nothing prevents loading the module, compile and load it. Nothing -> doLoadModule modName modName -- | Directives set via `:set`. evalCommand _output (Directive SetDynFlag flagsStr) state = safely state $ do write state $ "All Flags: " ++ flagsStr -- Find which flags are IHaskell flags, and which are GHC flags let flags = words flagsStr -- Get the kernel state updater for any IHaskell flag; Nothing for things that aren't IHaskell -- flags. ihaskellFlagUpdater :: String -> Maybe (KernelState -> KernelState) ihaskellFlagUpdater flag = getUpdateKernelState <$> find (elem flag . getSetName) kernelOpts (ihaskellFlags, ghcFlags) = partition (isJust . ihaskellFlagUpdater) flags write state $ "IHaskell Flags: " ++ unwords ihaskellFlags write state $ "GHC Flags: " ++ unwords ghcFlags if null flags then do flgs <- getSessionDynFlags return EvalOut { evalStatus = Success , evalResult = Display [ plain $ showSDoc flgs $ vcat [ pprDynFlags False flgs , pprLanguages False flgs ] ] , evalState = state , evalPager = [] , evalMsgs = [] } else do -- Apply all IHaskell flag updaters to the state to get the new state let state' = foldl' (.) id (mapMaybe ihaskellFlagUpdater ihaskellFlags) state errs <- setFlags ghcFlags let disp = case errs of [] -> mempty _ -> displayError $ intercalate "\n" errs -- For -XNoImplicitPrelude, remove the Prelude import. For -XImplicitPrelude, add it back in. if "-XNoImplicitPrelude" `elem` flags then evalImport "import qualified Prelude as Prelude" else when ("-XImplicitPrelude" `elem` flags) $ do importDecl <- parseImportDecl "import Prelude" let implicitPrelude = importDecl { ideclImplicit = True } imports <- getContext setContext $ IIDecl implicitPrelude : imports return EvalOut { evalStatus = Success , evalResult = disp , evalState = state' , evalPager = [] , evalMsgs = [] } evalCommand output (Directive SetExtension opts) state = do write state $ "Extension: " ++ opts let set = concatMap (" -X" ++) $ words opts evalCommand output (Directive SetDynFlag set) state evalCommand _output (Directive LoadModule mods) state = wrapExecution state $ do write state $ "Load Module: " ++ mods let stripped@(firstChar:remainder) = mods (modules, removeModule) = case firstChar of '+' -> (words remainder, False) '-' -> (words remainder, True) _ -> (words stripped, False) forM_ modules $ \modl -> if removeModule then removeImport modl else evalImport $ "import " ++ modl return mempty evalCommand _output (Directive SetOption opts) state = do write state $ "Option: " ++ opts let nonExisting = filter (not . optionExists) $ words opts if not $ null nonExisting then let err = "No such options: " ++ intercalate ", " nonExisting in return EvalOut { evalStatus = Failure , evalResult = displayError err , evalState = state , evalPager = [] , evalMsgs = [] } else let options = mapMaybe findOption $ words opts updater = foldl' (.) id $ map getUpdateKernelState options in return EvalOut { evalStatus = Success , evalResult = mempty , evalState = updater state , evalPager = [] , evalMsgs = [] } where optionExists = isJust . findOption findOption opt = find (elem opt . getOptionName) kernelOpts evalCommand _ (Directive GetType expr) state = wrapExecution state $ do write state $ "Type: " ++ expr formatType <$> ((expr ++ " :: ") ++) <$> getType expr evalCommand _ (Directive GetKind expr) state = wrapExecution state $ do write state $ "Kind: " ++ expr (_, kind) <- GHC.typeKind False expr flags <- getSessionDynFlags let typeStr = showSDocUnqual flags $ ppr kind return $ formatType $ expr ++ " :: " ++ typeStr evalCommand _ (Directive LoadFile names) state = wrapExecution state $ do write state $ "Load: " ++ names displays <- forM (words names) $ \name -> do let filename = if ".hs" `isSuffixOf` name then name else name ++ ".hs" contents <- liftIO $ readFile filename modName <- intercalate "." <$> getModuleName contents doLoadModule filename modName return (ManyDisplay displays) evalCommand publish (Directive ShellCmd cmd) state = wrapExecution state $ -- Assume the first character of 'cmd' is '!'. case words $ drop 1 cmd of "cd":dirs -> do -- Get home so we can replace '~` with it. homeEither <- liftIO (try $ getEnv "HOME" :: IO (Either SomeException String)) let home = case homeEither of Left _ -> "~" Right v -> v let directory = replace "~" home $ unwords dirs exists <- liftIO $ doesDirectoryExist directory if exists then do -- Set the directory in IHaskell native code, for future shell commands. This doesn't set it for -- user code, though. liftIO $ setCurrentDirectory directory -- Set the directory for user code. let cmd1 = printf "IHaskellDirectory.setCurrentDirectory \"%s\"" $ replace " " "\\ " $ replace "\"" "\\\"" directory _ <- execStmt cmd1 execOptions return mempty else return $ displayError $ printf "No such directory: '%s'" directory cmd1 -> liftIO $ do (pipe, hdl) <- createPipe let initProcSpec = shell $ unwords cmd1 procSpec = initProcSpec { std_in = Inherit , std_out = UseHandle hdl , std_err = UseHandle hdl } (_, _, _, process) <- createProcess procSpec -- Accumulate output from the process. outputAccum <- liftIO $ newMVar "" -- Start a loop to publish intermediate results. let -- Compute how long to wait between reading pieces of the output. `threadDelay` takes an -- argument of microseconds. ms = 1000 delay = 100 * ms -- Maximum size of the output (after which we truncate). maxSize = 100 * 1000 incSize = 200 output str = publish $ IntermediateResult $ Display [plain str] loop = do -- Wait and then check if the computation is done. threadDelay delay -- Read next chunk and append to accumulator. nextChunk <- readChars pipe "\n" incSize modifyMVar_ outputAccum (return . (++ nextChunk)) -- Check if we're done. mExitCode <- getProcessExitCode process case mExitCode of Nothing -> do -- Write to frontend and repeat. readMVar outputAccum >>= flip output Success loop Just exitCode -> do next <- readChars pipe "" maxSize modifyMVar_ outputAccum (return . (++ next)) out <- readMVar outputAccum case exitCode of ExitSuccess -> return $ Display [plain out] ExitFailure code -> do let errMsg = "Process exited with error code " ++ show code htmlErr = printf "%s" errMsg return $ Display [ plain $ out ++ "\n" ++ errMsg , html $ printf "%s" out ++ htmlErr ] loop -- This is taken largely from GHCi's info section in InteractiveUI. evalCommand _ (Directive GetHelp _) state = do write state "Help via :help or :?." return EvalOut { evalStatus = Success , evalResult = Display [out] , evalState = state , evalPager = [] , evalMsgs = [] } where out = plain $ intercalate "\n" [ "The following commands are available:" , " :extension - Enable a GHC extension." , " :extension No - Disable a GHC extension." , " :type - Print expression type." , " :info - Print all info for a name." , " :hoogle - Search for a query on Hoogle." , " :doc - Get documentation for an identifier via Hoogle." , " :set -XFlag -Wall - Set an option (like ghci)." , " :option - Set an option." , " :option no- - Unset an option." , " :?, :help - Show this help text." , " :sprint - Print a value without forcing evaluation." , "" , "Any prefix of the commands will also suffice, e.g. use :ty for :type." , "" , "Options:" , " lint – enable or disable linting." , " svg – use svg output (cannot be resized)." , " show-types – show types of all bound names" , " show-errors – display Show instance missing errors normally." , " pager – use the pager to display results of :info, :doc, :hoogle, etc." ] -- This is taken largely from GHCi's info section in InteractiveUI. evalCommand _ (Directive GetInfo str) state = safely state $ do write state $ "Info: " ++ str -- Get all the info for all the names we're given. strings <- unlines <$> getDescription str -- Make pager work without html by porting to newer architecture let htmlify str1 = html $ concat [ "
" , "" ] return EvalOut { evalStatus = Success , evalResult = mempty , evalState = state , evalPager = [plain strings, htmlify strings] , evalMsgs = [] } evalCommand _ (Directive SearchHoogle query) state = safely state $ do results <- liftIO $ Hoogle.search query return $ hoogleResults state results evalCommand _ (Directive GetDoc query) state = safely state $ do results <- liftIO $ Hoogle.document query return $ hoogleResults state results evalCommand _ (Directive SPrint binding) state = wrapExecution state $ do flags <- getSessionDynFlags contents <- liftIO $ newIORef [] let action = \_dflags _sev _srcspan _ppr _style msg -> modifyIORef' contents (showSDoc flags msg :) let flags' = flags { log_action = action } _ <- setSessionDynFlags flags' Debugger.pprintClosureCommand False False binding _ <- setSessionDynFlags flags sprint <- liftIO $ readIORef contents return $ formatType (unlines sprint) evalCommand output (Statement stmt) state = wrapExecution state $ evalStatementOrIO output state (CapturedStmt stmt) evalCommand output (Expression expr) state = do write state $ "Expression:\n" ++ expr -- Try to use `display` to convert our type into the output Dislay If typechecking fails and there -- is no appropriate typeclass instance, this will throw an exception and thus `attempt` will return -- False, and we just resort to plaintext. let displayExpr = printf "(IHaskell.Display.display (%s))" expr :: String #if MIN_VERSION_ghc(8,2,0) canRunDisplay <- attempt $ exprType TM_Inst displayExpr #else canRunDisplay <- attempt $ exprType displayExpr #endif -- Check if this is a widget. let widgetExpr = printf "(IHaskell.Display.Widget (%s))" expr :: String #if MIN_VERSION_ghc(8,2,0) isWidget <- attempt $ exprType TM_Inst widgetExpr #else isWidget <- attempt $ exprType widgetExpr #endif -- Check if this is a template haskell declaration let declExpr = printf "((id :: IHaskellTH.DecsQ -> IHaskellTH.DecsQ) (%s))" expr :: String let anyExpr = printf "((id :: IHaskellPrelude.Int -> IHaskellPrelude.Int) (%s))" expr :: String #if MIN_VERSION_ghc(8,2,0) isTHDeclaration <- liftM2 (&&) (attempt $ exprType TM_Inst declExpr) (not <$> attempt (exprType TM_Inst anyExpr)) #else isTHDeclaration <- liftM2 (&&) (attempt $ exprType declExpr) (not <$> attempt (exprType anyExpr)) #endif write state $ "Can Display: " ++ show canRunDisplay write state $ "Is Widget: " ++ show isWidget write state $ "Is Declaration: " ++ show isTHDeclaration if isTHDeclaration then -- If it typechecks as a DecsQ, we do not want to display the DecsQ, we just want the -- declaration made. do _ <- write state "Suppressing display for template haskell declaration" _ <- GHC.runDecls expr return EvalOut { evalStatus = Success , evalResult = mempty , evalState = state , evalPager = [] , evalMsgs = [] } else if canRunDisplay then -- Use the display. As a result, `it` is set to the output. useDisplay displayExpr else do -- Evaluate this expression as though it's just a statement. The output is bound to 'it', so we can -- then use it. evalOut <- evalCommand output (Statement expr) state let out = evalResult evalOut showErr = isShowError out -- If evaluation failed, return the failure. If it was successful, we may be able to use the -- IHaskellDisplay typeclass. return $ if not showErr || useShowErrors state then evalOut else postprocessShowError evalOut where -- Try to evaluate an action. Return True if it succeeds and False if it throws an exception. The -- result of the action is discarded. attempt :: Interpreter a -> Interpreter Bool attempt action = gcatch (action >> return True) failure where failure :: SomeException -> Interpreter Bool failure _ = return False -- Check if the error is due to trying to print something that doesn't implement the Show typeclass. isShowError (ManyDisplay _) = False isShowError (Display errs) = -- Note that we rely on this error message being 'type cleaned', so that `Show` is not displayed as -- GHC.Show.Show. This is also very fragile! "No instance for (Show" `isPrefixOf` msg && isInfixOf "print it" msg where msg = extractPlain errs isSvg (DisplayData mime _) = mime == MimeSvg removeSvg :: Display -> Display removeSvg (Display disps) = Display $ filter (not . isSvg) disps removeSvg (ManyDisplay disps) = ManyDisplay $ map removeSvg disps useDisplay _displayExpr = do -- If there are instance matches, convert the object into a Display. We also serialize it into a -- bytestring. We get the bytestring IO action as a dynamic and then convert back to a bytestring, -- which we promptly unserialize. Note that attempting to do this without the serialization to -- binary and back gives very strange errors - all the types match but it refuses to decode back -- into a Display. Suppress output, so as not to mess up console. First, evaluate the expression in -- such a way that we have access to `it`. io <- isIO expr let stmtTemplate = if io then "it <- (%s)" else "let { it = %s }" evalOut <- evalCommand output (Statement $ printf stmtTemplate expr) state case evalStatus evalOut of Failure -> return evalOut Success -> wrapExecution state $ do -- Compile the display data into a bytestring. let cexpr = "fmap IHaskell.Display.serializeDisplay (IHaskell.Display.display it)" displayedBytestring <- dynCompileExpr cexpr -- Convert from the bytestring into a display. case fromDynamic displayedBytestring of Nothing -> error "Expecting lazy Bytestring" Just bytestringIO -> do bytestring <- liftIO bytestringIO case Serialize.decode bytestring of Left err -> error err Right disp -> return $ if useSvg state then disp :: Display else removeSvg disp #if MIN_VERSION_ghc(8,2,0) isIO exp = attempt $ exprType TM_Inst $ printf "((\\x -> x) :: IO a -> IO a) (%s)" exp #else isIO exp = attempt $ exprType $ printf "((\\x -> x) :: IO a -> IO a) (%s)" exp #endif postprocessShowError :: EvalOut -> EvalOut postprocessShowError evalOut = evalOut { evalResult = Display $ map postprocess disps } where Display disps = evalResult evalOut txt = extractPlain disps postprocess (DisplayData MimeHtml _) = html $ printf fmt unshowableType (formatErrorWithClass "err-msg collapse" txt) script where fmt = "
Unshowable:%s%s
" script = unlines [ "$('#unshowable').on('click', function(e) {" , " e.preventDefault();" , " var $this = $(this);" , " var $collapse = $this.closest('.collapse-group').find('.err-msg');" , " $collapse.collapse('toggle');" , "});" ] postprocess other = other unshowableType = fromMaybe "" $ do let pieces = words txt before = takeWhile (/= "arising") pieces after = init $ unwords $ tail $ dropWhile (/= "(Show") before firstChar <- headMay after return $ if firstChar == '(' then init $ tail after else after evalCommand _ (Declaration decl) state = wrapExecution state $ do write state $ "Declaration:\n" ++ decl boundNames <- evalDeclarations decl let nonDataNames = filter (not . isUpper . head) boundNames -- Display the types of all bound names if the option is on. This is similar to GHCi :set +t. if not $ useShowTypes state then return mempty else do -- Get all the type strings. dflags <- getSessionDynFlags types <- forM nonDataNames $ \name -> do #if MIN_VERSION_ghc(8,2,0) theType <- showSDocUnqual dflags . ppr <$> exprType TM_Inst name #else theType <- showSDocUnqual dflags . ppr <$> exprType name #endif return $ name ++ " :: " ++ theType return $ Display [html $ unlines $ map formatGetType types] evalCommand _ (TypeSignature sig) state = wrapExecution state $ -- We purposefully treat this as a "success" because that way execution continues. Empty type -- signatures are likely due to a parse error later on, and we want that to be displayed. return $ displayError $ "The type signature " ++ sig ++ "\nlacks an accompanying binding." evalCommand _ (ParseError loc err) state = do write state "Parse Error." return EvalOut { evalStatus = Failure , evalResult = displayError $ formatParseError loc err , evalState = state , evalPager = [] , evalMsgs = [] } evalCommand _ (Pragma (PragmaUnsupported pragmaType) _pragmas) state = wrapExecution state $ return $ displayError $ "Pragmas of type " ++ pragmaType ++ "\nare not supported." evalCommand output (Pragma PragmaLanguage pragmas) state = do write state $ "Got LANGUAGE pragma " ++ show pragmas evalCommand output (Directive SetExtension $ unwords pragmas) state hoogleResults :: KernelState -> [Hoogle.HoogleResult] -> EvalOut hoogleResults state results = EvalOut { evalStatus = Success , evalResult = mempty , evalState = state , evalPager = [ plain $ unlines $ map (Hoogle.render Hoogle.Plain) results , html $ unlines $ map (Hoogle.render Hoogle.HTML) results ] , evalMsgs = [] } doLoadModule :: String -> String -> Ghc Display doLoadModule name modName = do -- Remember which modules we've loaded before. importedModules <- getContext flip gcatch (unload importedModules) $ do -- Compile loaded modules. flags <- getSessionDynFlags errRef <- liftIO $ newIORef [] _ <- setSessionDynFlags $ flip gopt_set Opt_BuildDynamicToo flags { hscTarget = objTarget flags , log_action = \_dflags _sev _srcspan _ppr _style msg -> modifyIORef' errRef (showSDoc flags msg :) } -- Load the new target. target <- guessTarget name Nothing oldTargets <- getTargets -- Add a target, but make sure targets are unique! addTarget target getTargets >>= return . nubBy ((==) `on` targetId) >>= setTargets result <- load LoadAllTargets -- Reset the context, since loading things screws it up. initializeItVariable -- Reset targets if we failed. case result of Failed -> setTargets oldTargets Succeeded{} -> return () -- Add imports setContext $ case result of Failed -> importedModules Succeeded -> IIDecl (simpleImportDecl $ mkModuleName modName) : importedModules -- Switch back to interpreted mode. _ <- setSessionDynFlags flags case result of Succeeded -> return mempty Failed -> do errorStrs <- unlines <$> reverse <$> liftIO (readIORef errRef) return $ displayError $ "Failed to load module " ++ modName ++ "\n" ++ errorStrs where unload :: [InteractiveImport] -> SomeException -> Ghc Display unload imported exception = do print $ show exception -- Explicitly clear targets setTargets [] _ <- load LoadAllTargets -- Switch to interpreted mode! flags <- getSessionDynFlags _ <- setSessionDynFlags flags { hscTarget = HscInterpreted } -- Return to old context, make sure we have `it`. setContext imported initializeItVariable return $ displayError $ "Failed to load module " ++ modName ++ ": " ++ show exception objTarget :: DynFlags -> HscTarget #if MIN_VERSION_ghc(8,10,0) objTarget = defaultObjectTarget #else objTarget flags = defaultObjectTarget $ targetPlatform flags #endif data Captured a = CapturedStmt String | CapturedIO (IO a) capturedEval :: (String -> IO ()) -- ^ Function used to publish intermediate output. -> Captured a -- ^ Statement to evaluate. -> Interpreter (String, ExecResult) -- ^ Return the output and result. capturedEval output stmt = do -- Generate random variable names to use so that we cannot accidentally override the variables by -- using the right names in the terminal. gen <- liftIO getStdGen let -- Variable names generation. rand = take 20 $ randomRs ('0', '9') gen var name = name ++ rand -- Variables for the pipe input and outputs. readVariable = var "file_read_var_" writeVariable = var "file_write_var_" -- Variable where to store old stdout. oldVariableStdout = var "old_var_stdout_" -- Variable where to store old stderr. oldVariableStderr = var "old_var_stderr_" -- Variable used to store true `it` value. itVariable = var "it_var_" voidpf str = printf $ str ++ " IHaskellPrelude.>> IHaskellPrelude.return ()" -- Statements run before the thing we're evaluating. initStmts = [ printf "let %s = it" itVariable , printf "(%s, %s) <- IHaskellIO.createPipe" readVariable writeVariable , printf "%s <- IHaskellIO.dup IHaskellIO.stdOutput" oldVariableStdout , printf "%s <- IHaskellIO.dup IHaskellIO.stdError" oldVariableStderr , voidpf "IHaskellIO.dupTo %s IHaskellIO.stdOutput" writeVariable , voidpf "IHaskellIO.dupTo %s IHaskellIO.stdError" writeVariable , voidpf "IHaskellSysIO.hSetBuffering IHaskellSysIO.stdout IHaskellSysIO.NoBuffering" , voidpf "IHaskellSysIO.hSetBuffering IHaskellSysIO.stderr IHaskellSysIO.NoBuffering" , printf "let it = %s" itVariable ] -- Statements run after evaluation. postStmts = [ printf "let %s = it" itVariable , voidpf "IHaskellSysIO.hFlush IHaskellSysIO.stdout" , voidpf "IHaskellSysIO.hFlush IHaskellSysIO.stderr" , voidpf "IHaskellIO.dupTo %s IHaskellIO.stdOutput" oldVariableStdout , voidpf "IHaskellIO.dupTo %s IHaskellIO.stdError" oldVariableStderr , voidpf "IHaskellIO.closeFd %s" writeVariable , printf "let it = %s" itVariable ] goStmt :: String -> Ghc ExecResult goStmt s = execStmt s execOptions runWithResult (CapturedStmt str) = goStmt str runWithResult (CapturedIO io) = do stat <- gcatch (liftIO io >> return NoException) (return . AnyException) return $ case stat of NoException -> ExecComplete (Right []) 0 AnyException e -> ExecComplete (Left e) 0 -- Initialize evaluation context. forM_ initStmts goStmt -- This works fine on GHC 8.0 and newer dyn <- dynCompileExpr readVariable pipe <- case fromDynamic dyn of Nothing -> error "Evaluate: Bad pipe" Just fd -> liftIO $ do hdl <- fdToHandle fd hSetEncoding hdl utf8 return hdl -- Keep track of whether execution has completed. completed <- liftIO $ newMVar False finishedReading <- liftIO newEmptyMVar outputAccum <- liftIO $ newMVar "" -- Start a loop to publish intermediate results. let -- Compute how long to wait between reading pieces of the output. `threadDelay` takes an -- argument of microseconds. ms = 1000 delay = 100 * ms -- Maximum size of the output (after which we truncate). maxSize = 100 * 1000 loop = do -- Wait and then check if the computation is done. threadDelay delay computationDone <- readMVar completed if not computationDone then do -- Read next chunk and append to accumulator. nextChunk <- readChars pipe "\n" 100 modifyMVar_ outputAccum (return . (++ nextChunk)) -- Write to frontend and repeat. readMVar outputAccum >>= output loop else do -- Read remainder of output and accumulate it. nextChunk <- readChars pipe "" maxSize modifyMVar_ outputAccum (return . (++ nextChunk)) -- We're done reading. putMVar finishedReading True _ <- liftIO $ forkIO loop result <- gfinally (runWithResult stmt) $ do -- Execution is done. liftIO $ modifyMVar_ completed (const $ return True) -- Finalize evaluation context. void $ forM postStmts goStmt -- Once context is finalized, reading can finish. Wait for reading to finish to that the output -- accumulator is completely filled. liftIO $ takeMVar finishedReading printedOutput <- liftIO $ readMVar outputAccum return (printedOutput, result) data AnyException = NoException | AnyException SomeException capturedIO :: Publisher -> KernelState -> IO a -> Interpreter Display capturedIO publish state action = do let showError = return . displayError . show handler e@SomeException{} = showError e gcatch (evalStatementOrIO publish state (CapturedIO action)) handler -- | Evaluate a @Captured@, and then publish the final result to the frontend. Returns the final -- Display. evalStatementOrIO :: Publisher -> KernelState -> Captured a -> Interpreter Display evalStatementOrIO publish state cmd = do let output str = publish . IntermediateResult $ Display [plain str] case cmd of CapturedStmt stmt -> write state $ "Statement:\n" ++ stmt CapturedIO _ -> write state "Evaluating Action" (printed, result) <- capturedEval (flip output Success) cmd case result of ExecComplete (Right names) _ -> do dflags <- getSessionDynFlags let allNames = map (showPpr dflags) names isItName name = name == "it" || name == "it" ++ show (getExecutionCounter state) nonItNames = filter (not . isItName) allNames oput = [ plain printed | not . null $ strip printed ] write state $ "Names: " ++ show allNames -- Display the types of all bound names if the option is on. This is similar to GHCi :set +t. if not $ useShowTypes state then return $ Display oput else do -- Get all the type strings. types <- forM nonItNames $ \name -> do #if MIN_VERSION_ghc(8,2,0) theType <- showSDocUnqual dflags . ppr <$> exprType TM_Inst name #else theType <- showSDocUnqual dflags . ppr <$> exprType name #endif return $ name ++ " :: " ++ theType let joined = unlines types htmled = unlines $ map formatGetType types return $ case extractPlain oput of "" -> Display [html htmled] -- Return plain and html versions. Previously there was only a plain version. txt -> Display [plain $ joined ++ "\n" ++ txt, html $ htmled ++ mono txt] ExecComplete (Left exception) _ -> throw exception ExecBreak{} -> error "Should not break." -- Read from a file handle until we hit a delimiter or until we've read as many characters as -- requested readChars :: Handle -> String -> Int -> IO String readChars _handle _delims 0 = -- If we're done reading, return nothing. return [] readChars hdl delims nchars = do -- Try reading a single character. It will throw an exception if the handle is already closed. tryRead <- gtry $ hGetChar hdl :: IO (Either SomeException Char) case tryRead of Right ch -> -- If this is a delimiter, stop reading. if ch `elem` delims then return [ch] else do next <- readChars hdl delims (nchars - 1) return $ ch : next -- An error occurs at the end of the stream, so just stop reading. Left _ -> return [] formatError :: ErrMsg -> String formatError = formatErrorWithClass "err-msg" formatErrorWithClass :: String -> ErrMsg -> String formatErrorWithClass cls = printf "%s" cls . replace "\n" "
" . fixDollarSigns . replace "<" "<" . replace ">" ">" . replace "&" "&" . replace useDashV "" . replace "Ghci" "IHaskell" . replace "‘interactive:" "‘" . rstrip . typeCleaner where fixDollarSigns = replace "$" "$" useDashV = "\n Use -v to see a list of the files searched for." formatParseError :: StringLoc -> String -> ErrMsg formatParseError (Loc ln col) = printf "Parse error (line %d, column %d): %s" ln col formatGetType :: String -> String formatGetType = printf "%s" formatType :: String -> Display formatType typeStr = Display [plain typeStr, html $ formatGetType typeStr] displayError :: ErrMsg -> Display displayError msg = Display [plain . typeCleaner $ msg, html $ formatError msg] mono :: String -> String mono = printf "%s"