haskell-language-server-2.15.0.0: plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wwarn #-}
-- | Expression execution
module Ide.Plugin.Eval.Code (
Statement
, evalExprRanges
, resultRange
, propSetup
, evalExprCheck
, asStatements
, execStmtCaptureResult
) where
import Control.Lens ((^.))
import Control.Monad.Catch (MonadMask, bracket)
import Control.Monad.IO.Class
import Data.Algorithm.Diff (Diff, PolyDiff (..), getDiff)
import Data.List (dropWhileEnd)
import qualified Data.List.NonEmpty as NE
import Data.Maybe (listToMaybe)
import Data.String (IsString)
import qualified Data.Text as T
import Development.IDE.GHC.Compat
import GHC (ExecOptions, ExecResult (..),
execStmt)
import Ide.Logger (Recorder, WithPriority, logWith)
import qualified Ide.Logger as Log
import Ide.Plugin.Eval.Types (EvalExpr (..), Language (Plain),
Loc, Located (..), Log (..),
Section (sectionLanguage), Txt,
locate, locate0)
import Ide.Plugin.Eval.Util (gStrictTry)
import qualified Language.LSP.Protocol.Lens as L
import Language.LSP.Protocol.Types (Position (Position),
Range (Range))
import System.IO.Extra (newTempFile, readFile')
-- | Return the ranges of the expression and result parts of the given 'EvalExpr'.
evalExprRanges :: EvalExpr -> (Range, Range)
evalExprRanges tst =
let startLine = evalExprRange tst ^. L.start . L.line
(fromIntegral -> exprLines, fromIntegral -> resultLines) = evalExprLengths tst
resLine = startLine + exprLines
in ( Range
(Position startLine 0)
(Position resLine 0)
, Range (Position resLine 0) (Position (resLine + resultLines) 0)
)
-- | The document range where the result of the 'EvalExpr' is defined.
resultRange :: EvalExpr -> Range
resultRange = snd . evalExprRanges
{- |
>>> showDiffs $ getDiff ["abc" :: String,"def","ghi","end"] ["abc","def","Z","ZZ","end"]
["abc","def","WAS ghi","NOW Z","NOW ZZ","end"]
-}
showDiffs :: (Semigroup a, IsString a) => [Diff a] -> [a]
showDiffs = map showDiff
showDiff :: (Semigroup a, IsString a) => Diff a -> a
showDiff (First w) = "WAS " <> w
showDiff (Second w) = "NOW " <> w
showDiff (Both w _) = w
-- | Compare the expected output recorded in the 'EvalExpr' with the actual
-- output @out@, returning the result as one 'T.Text' per line. When diffing is
-- enabled and there is a recorded output, return a line-by-line diff (see
-- 'showDiffs'); otherwise return @out@ unchanged.
--
-- @out@ is normalised to one element per line first: a multi-line result
-- arrives here as a single element with embedded newlines, whereas the recorded
-- output is already split per line, and 'getDiff' compares element-wise. Without
-- this, identical multi-line results would be reported as entirely changed.
evalExprCheck :: Bool -> (Section, EvalExpr) -> [T.Text] -> [T.Text]
evalExprCheck diff (section, evalExpr) out
| not diff
|| null (evalExprOutput evalExpr)
|| sectionLanguage section == Plain =
outLines
| otherwise =
showDiffs $
getDiff (map T.pack $ evalExprOutput evalExpr) outLines
where
outLines = concatMap T.lines out
-- | The number of (expression lines, result lines) an 'EvalExpr' occupies.
evalExprLengths :: EvalExpr -> (Int, Int)
evalExprLengths (Example e r _) = (NE.length e, length r)
evalExprLengths (Property _ r _) = (1, length r)
-- | A one-line Haskell statement
type Statement = Loc String
-- | The Haskell statements to feed to GHCi for an 'EvalExpr', each tagged with
-- its source line so evaluation errors can be located.
asStatements :: EvalExpr -> [Statement]
asStatements lt =
locate $ Located (fromIntegral $ evalExprRange lt ^. L.start . L.line) (asStmts lt)
-- | The raw statement lines of an 'EvalExpr'. A 'Property' is wrapped so its
-- result is evaluated through 'propEvaluation' (see 'propSetup').
asStmts :: EvalExpr -> [Txt]
asStmts (Example e _ _) = NE.toList e
asStmts (Property t _ _) =
["prop11 = " ++ t, "(propEvaluation prop11 :: IO String)"]
-- | A wrapper of 'InteractiveEval.execStmt', capturing the execution result
execStmtCaptureResult ::
Recorder (WithPriority Log)
-> String
-> ExecOptions
-> Ghc (Either String (Maybe String))
execStmtCaptureResult recorder stmt opts = do
(result, (output, execResultE)) <-
withCaptureResult recorder $
withCaptureStdHandles opts $
gStrictTry (execStmt stmt opts)
case execResultE of
Left exc ->
pure $ Left exc
Right (ExecComplete (Left err) _) ->
pure $ Left $ show err
Right (ExecComplete (Right _) _) -> do
pure $ Right $ toMaybe (combine output result)
Right ExecBreak{} ->
pure $ Right $ Just "breakpoints are not supported"
where
toMaybe :: String -> Maybe String
toMaybe x | null x = Nothing
| otherwise = Just x
-- Join the captured stdout/stderr output with the result value. GHC
-- diagnostics (e.g. warnings) written to stderr end with a trailing blank
-- line; drop trailing newlines so output and result are separated by a
-- single newline rather than a spurious blank line (which would otherwise
-- surface unexpectedly in the rendered result).
combine :: String -> String -> String
combine output result
| null trimmed = result
| null result = trimmed
| otherwise = trimmed <> "\n" <> result
where
trimmed = dropWhileEnd (== '\n') output
-- 'System.IO.Extra.withTempFile' is specialized to 'IO'.
withTempFile :: (MonadIO m, MonadMask m) => (FilePath -> m b) -> m (String, b)
withTempFile k = do
bracket
(liftIO newTempFile)
(\(_, purgeTempFile) -> liftIO purgeTempFile)
(\(tempFile, _) -> do
r <- k tempFile
o <- liftIO $ readFile' tempFile
pure (o, r))
-- | Capture the value the statement evaluates to (printed by GHCi via the
-- interactive print function) by writing it to a temporary file.
withCaptureResult :: Recorder (WithPriority Log) -> Ghc a -> Ghc (String, a)
withCaptureResult recorder action = withTempFile $ \resultTemp -> do
mEvalPrint <-
listToMaybe <$>
runDecls
("evalPrint x = P.writeFile " <> show resultTemp <> " (P.show x)")
case mEvalPrint of
Nothing ->
logWith recorder Log.Warning $ LogEvalFailedSettingInteractivePrintFunction
Just evalPrint ->
modifySession $
\hsc -> hsc {hsc_IC = setInteractivePrintName (hsc_IC hsc) evalPrint}
action
-- | Capture output written to @stdout@ and @stderr@ as a side effect of
-- evaluating the statement.
--
-- We redirect the handles from *within* the interpreted program, because the
-- statement writes to the interpreted standard handles, which are not the host
-- handles that 'System.IO.Silently' would redirect -- HLS has already
-- redirected the latter to protect the LSP channel (see
-- 'Development.IDE.Main').
--
-- NB: This redirection is process global, so output written concurrently to
-- @stdout@/@stderr@ from another thread may be captured here, or eval output
-- may leak. base provides no per-thread standard handles, so this is
-- unavoidable with this approach.
withCaptureStdHandles ::
ExecOptions
-> Ghc a
-> Ghc (String, a)
withCaptureStdHandles opts action = withTempFile $ \outputTemp -> do
bracket
(execStmt (captureSetup outputTemp) opts)
-- Restore the handles no matter how the statement terminated.
(\_ -> execStmt captureTeardown opts)
(\_ -> action)
-- Open a temporary file and redirect the interpreted @stdout@/@stderr@ to
-- it, saving the original handles in interactive bindings so 'captureTeardown'
-- can restore them. Bound to a tuple (rather than evaluated as a bare
-- expression) so GHCi does not pass it to the interactive print function.
captureSetup :: FilePath -> String
-- Squeeze into one line (executed by GHCi).
captureSetup outputTemp = unwords
[ "(__hls_captureHandle, __hls_savedStdout, __hls_savedStderr) <- do {"
, " __hls_h <- System.IO.openFile", show outputTemp, "System.IO.WriteMode;"
, " System.IO.hSetBuffering __hls_h System.IO.LineBuffering;"
, " __hls_o <- GHC.IO.Handle.hDuplicate System.IO.stdout;"
, " __hls_e <- GHC.IO.Handle.hDuplicate System.IO.stderr;"
, " GHC.IO.Handle.hDuplicateTo __hls_h System.IO.stdout;"
, " GHC.IO.Handle.hDuplicateTo __hls_h System.IO.stderr;"
, " P.return (__hls_h, __hls_o, __hls_e);"
, " }"
]
-- Flush, restore the original handles and close the temporary file so the
-- host can read back the captured output.
captureTeardown :: String
-- Squeeze into one line (executed by GHCi).
captureTeardown = unwords
[ "__hls_restored <- do {"
, " System.IO.hFlush System.IO.stdout;"
, " System.IO.hFlush System.IO.stderr;"
, " GHC.IO.Handle.hDuplicateTo __hls_savedStdout System.IO.stdout;"
, " GHC.IO.Handle.hDuplicateTo __hls_savedStderr System.IO.stderr;"
, " System.IO.hClose __hls_savedStdout;"
, " System.IO.hClose __hls_savedStderr;"
, " System.IO.hClose __hls_captureHandle;"
, " }"
]
{- | GHC declarations required to evaluate property 'EvalExprs'
Example:
prop> \(l::[Bool]) -> reverse (reverse l) == l
+++ OK, passed 100 tests.
prop> \(l::[Bool]) -> reverse l == l
*** Failed! Falsified (after 4 tests and 1 shrink):
[True,False]
-}
propSetup :: [Loc [Char]]
propSetup =
locate0
[ ":set -XScopedTypeVariables -XExplicitForAll"
, "import qualified Test.QuickCheck as Q11"
, "propEvaluation p = Q11.quickCheckWithResult Q11.stdArgs p >>= error . Q11.output" -- uses `error` to get a multi-line display
]