Skip to content

Commit a1c7d21

Browse files
committed
eval: update readme; clean code; fix some bugs
- Correctly treat empty output or result when combining both - Do not handle blank lines in any special way - Update Readme of `hls-eval-plugin`
1 parent 05c315a commit a1c7d21

5 files changed

Lines changed: 68 additions & 141 deletions

File tree

plugins/hls-eval-plugin/README.md

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -259,41 +259,19 @@ This is a problem if you want, for example, to pretty print a value (in this cas
259259
"[ 1\n, 2\n, 3\n]"
260260
```
261261

262-
We could try to print the pretty-print output, but stdout is not captured so we get just a ():
263-
264-
```
265-
>>> print $ pShowNoColor [1..7]
266-
()
267-
```
268-
269-
To display it properly, we can exploit the fact that the output of an error is displayed as a multi-line text:
270-
262+
Instead, we can do
271263
```
272-
>>> import qualified Data.Text.Lazy as TL
273-
>>> import Text.Pretty.Simple
274-
>>> prettyPrint v = error (TL.unpack $ pShowNoColor v) :: IO String
275-
>>> prettyPrint [1..3]
276-
[ 1
277-
, 2
278-
, 3
279-
]
264+
>>> mapM_ putStrLn $ lines pShowNoColor [1..7]
265+
-- [ 1
266+
-- , 2
267+
-- , 3
268+
-- ]
280269
```
281270

282-
This assumes you did not turn on exception marking (see [Marking exceptions](#marking-exceptions) below).
283-
284271
# Differences with doctest
285272

286273
Though the Eval plugin functionality is quite similar to that of [doctest](https://hackage.haskell.org/package/doctest), some doctest's features are not supported.
287274

288-
### Capturing Stdout
289-
290-
Only the value of an IO expression is spliced in, not its output:
291-
292-
```
293-
>>> print "foo"
294-
()
295-
```
296-
297275
### Marking exceptions
298276

299277
When an exception is thrown it is not prefixed:

plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Control.Lens ((^.))
1717
import Control.Monad.Catch (MonadMask, bracket)
1818
import Control.Monad.IO.Class
1919
import Data.Algorithm.Diff (Diff, PolyDiff (..), getDiff)
20+
import Data.List (dropWhileEnd)
2021
import qualified Data.List.NonEmpty as NE
2122
import Data.Maybe (listToMaybe)
2223
import Data.String (IsString)
@@ -45,7 +46,6 @@ evalExprRanges tst =
4546
resLine = startLine + exprLines
4647
in ( Range
4748
(Position startLine 0)
48-
--(Position (startLine + exprLines + resultLines) 0),
4949
(Position resLine 0)
5050
, Range (Position resLine 0) (Position (resLine + resultLines) 0)
5151
)
@@ -54,16 +54,15 @@ evalExprRanges tst =
5454
resultRange :: EvalExpr -> Range
5555
resultRange = snd . evalExprRanges
5656

57-
-- TODO: handle BLANKLINE
58-
{-
59-
>>> showDiffs $ getDiff ["abc","def","ghi","end"] ["abc","def","Z","ZZ","end"]
57+
{- |
58+
>>> showDiffs $ getDiff ["abc" :: String,"def","ghi","end"] ["abc","def","Z","ZZ","end"]
6059
["abc","def","WAS ghi","NOW Z","NOW ZZ","end"]
6160
-}
6261
showDiffs :: (Semigroup a, IsString a) => [Diff a] -> [a]
6362
showDiffs = map showDiff
6463

6564
showDiff :: (Semigroup a, IsString a) => Diff a -> a
66-
showDiff (First w) = "WAS " <> w
65+
showDiff (First w) = "WAS " <> w
6766
showDiff (Second w) = "NOW " <> w
6867
showDiff (Both w _) = w
6968

@@ -78,8 +77,13 @@ showDiff (Both w _) = w
7877
-- this, identical multi-line results would be reported as entirely changed.
7978
evalExprCheck :: Bool -> (Section, EvalExpr) -> [T.Text] -> [T.Text]
8079
evalExprCheck diff (section, evalExpr) out
81-
| not diff || null (evalExprOutput evalExpr) || sectionLanguage section == Plain = outLines
82-
| otherwise = showDiffs $ getDiff (map T.pack $ evalExprOutput evalExpr) outLines
80+
| not diff
81+
|| null (evalExprOutput evalExpr)
82+
|| sectionLanguage section == Plain =
83+
outLines
84+
| otherwise =
85+
showDiffs $
86+
getDiff (map T.pack $ evalExprOutput evalExpr) outLines
8387
where
8488
outLines = concatMap T.lines out
8589

@@ -88,7 +92,7 @@ evalExprLengths :: EvalExpr -> (Int, Int)
8892
evalExprLengths (Example e r _) = (NE.length e, length r)
8993
evalExprLengths (Property _ r _) = (1, length r)
9094

91-
-- |A one-line Haskell statement
95+
-- | A one-line Haskell statement
9296
type Statement = Loc String
9397

9498
-- | The Haskell statements to feed to GHCi for an 'EvalExpr', each tagged with
@@ -121,14 +125,27 @@ execStmtCaptureResult recorder stmt opts = do
121125
Right (ExecComplete (Left err) _) ->
122126
pure $ Left $ show err
123127
Right (ExecComplete (Right _) _) -> do
124-
pure $ Right $ toMaybe (output <> result)
128+
pure $ Right $ toMaybe (combine output result)
125129
Right ExecBreak{} ->
126130
pure $ Right $ Just "breakpoints are not supported"
127131
where
128132
toMaybe :: String -> Maybe String
129133
toMaybe x | null x = Nothing
130134
| otherwise = Just x
131135

136+
-- Join the captured stdout/stderr output with the result value. GHC
137+
-- diagnostics (e.g. warnings) written to stderr end with a trailing blank
138+
-- line; drop trailing newlines so output and result are separated by a
139+
-- single newline rather than a spurious blank line (which would otherwise
140+
-- surface unexpectedly in the rendered result).
141+
combine :: String -> String -> String
142+
combine output result
143+
| null trimmed = result
144+
| null result = trimmed
145+
| otherwise = trimmed <> "\n" <> result
146+
where
147+
trimmed = dropWhileEnd (== '\n') output
148+
132149
-- 'System.IO.Extra.withTempFile' is specialized to 'IO'.
133150
withTempFile :: (MonadIO m, MonadMask m) => (FilePath -> m b) -> m (String, b)
134151
withTempFile k = do
@@ -219,10 +236,10 @@ captureTeardown = unwords
219236
Example:
220237
221238
prop> \(l::[Bool]) -> reverse (reverse l) == l
222-
+++ OK, passed 100 evalExprs.
239+
+++ OK, passed 100 tests.
223240
224241
prop> \(l::[Bool]) -> reverse l == l
225-
*** Failed! Falsified (after 6 evalExprs and 2 shrinks):
242+
*** Failed! Falsified (after 4 tests and 1 shrink):
226243
[True,False]
227244
-}
228245
propSetup :: [Loc [Char]]

plugins/hls-eval-plugin/src/Ide/Plugin/Eval/GHC.hs

Lines changed: 4 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -30,41 +30,12 @@ import GHC (setTopSessionDynFlags,
3030
import GHC.Driver.Env
3131
import GHC.Driver.Session (getDynFlags)
3232

33-
{- $setup
34-
>>> import GHC
35-
>>> import GHC.Paths
36-
>>> run act = runGhc (Just libdir) (getInteractiveDynFlags >>= act)
37-
>>> libdir
38-
"/Users/titto/.ghcup/ghc/8.8.4/lib/ghc-8.8.4"
39-
-}
40-
41-
{- | True if specified package is present in DynFlags
42-
43-
-- >>> hasPackageTst pkg = run $ \df -> return (hasPackage df pkg)
44-
>>> hasPackageTst pkg = run $ \_ -> addPackages [pkg] >>= return . either Left (\df -> Right (hasPackage df pkg))
45-
46-
>>> hasPackageTst "base"
47-
Right True
48-
49-
>>> hasPackageTst "ghc"
50-
Right True
51-
52-
>>> hasPackageTst "extra"
53-
Left "<command line>: cannot satisfy -package extra\n (use -v for more information)"
54-
55-
>>> hasPackageTst "QuickCheck"
56-
Left "<command line>: cannot satisfy -package QuickCheck\n (use -v for more information)"
57-
-}
5833
hasPackage :: DynFlags -> String -> Bool
5934
hasPackage df = hasPackage_ (packageFlags df)
6035

6136
hasPackage_ :: [PackageFlag] -> [Char] -> Bool
6237
hasPackage_ pkgFlags name = any (name `isPrefixOf`) (pkgNames_ pkgFlags)
6338

64-
{- |
65-
>>> run (return . pkgNames)
66-
[]
67-
-}
6839
pkgNames :: DynFlags -> [String]
6940
pkgNames = pkgNames_ . packageFlags
7041

@@ -77,27 +48,6 @@ pkgNames_ =
7748
_ -> Nothing
7849
)
7950

80-
{- | Expose a list of packages.
81-
>>> addPackagesTest pkgs = run (\_ -> (packageFlags <$>) <$> addPackages pkgs)
82-
83-
>>> addPackagesTest []
84-
Right []
85-
86-
>>> addPackagesTest ["base","base","array"]
87-
Right [-package base{package base True ([])},-package array{package array True ([])}]
88-
89-
>>> addPackagesTest ["Cabal"]
90-
Right [-package Cabal{package Cabal True ([])}]
91-
92-
>>> addPackagesTest ["QuickCheck"]
93-
Left "<command line>: cannot satisfy -package QuickCheck\n (use -v for more information)"
94-
95-
>>> addPackagesTest ["base","notThere"]
96-
Left "<command line>: cannot satisfy -package notThere\n (use -v for more information)"
97-
98-
prop> \(x::Int) -> x + x == 2 * x
99-
+++ OK, passed 100 tests.
100-
-}
10151
addPackages :: [String] -> Ghc (Either String DynFlags)
10252
addPackages pkgNames = gStrictTry $
10353
modifyFlags $ \df ->
@@ -111,37 +61,15 @@ modifyFlags f = do
11161
_ <- setSessionDynFlags (f df)
11262
getSessionDynFlags
11363

114-
-- modifyFlags f = do
115-
-- modifyDynFlags f
116-
-- getSessionDynFlags
117-
118-
{- | Add import to evaluation context
119-
120-
>>> run $ \_ -> addImport "import Data.Maybe"
121-
Could not find module ‘Data.Maybe’
122-
Use -v (or `:set -v` in ghci) to see a list of the files searched for.
123-
124-
>>> run $ \df -> addPackages ["base"] >> addImport "import Data.Maybe"
125-
[import Data.Maybe]
126-
127-
>>> run $ \df -> addPackages ["base"] >> addImport "import qualified Data.Maybe as M"
128-
[import qualified Data.Maybe as M]
129-
-}
64+
-- | Add import to evaluation context
13065
addImport :: GhcMonad m => String -> m [InteractiveImport]
13166
addImport i = do
13267
ctx <- getContext
133-
-- dbgO "CONTEXT" ctx
13468
idecl <- parseImportDecl i
13569
setContext $ IIDecl idecl : ctx
136-
-- ctx' <- getContext
137-
-- dbg "CONTEXT'" ctx'
13870
getContext
13971

140-
{- | Add extension to interactive evaluation session
141-
>>> import GHC.LanguageExtensions.Type(Extension(..))
142-
>>> run $ \_ -> addExtension DeriveGeneric
143-
()
144-
-}
72+
-- | Add extension to interactive evaluation session
14573
addExtension :: GhcMonad m => Extension -> m ()
14674
addExtension ext =
14775
modifySession $ \hsc -> hsc{hsc_IC = setExtension (hsc_IC hsc) ext}
@@ -151,20 +79,16 @@ setExtension ic ext = ic{ic_dflags = xopt_set (ic_dflags ic) ext}
15179

15280
deriving instance Read Extension
15381

154-
-- Partial display of DynFlags contents, for testing purposes
82+
-- | Partial display of DynFlags contents, for testing purposes
15583
showDynFlags :: DynFlags -> String
15684
showDynFlags df =
15785
T.unpack . printOutputable . vcat . map (\(n, d) -> text (n ++ ": ") <+> d) $
15886
[ ("extensions", ppr . extensions $ df)
15987
, ("extensionFlags", ppr . EnumSet.toList . extensionFlags $ df)
16088
, ("importPaths", vList $ importPaths df)
16189
, ("generalFlags", pprHsString . fromString . show . EnumSet.toList . generalFlags $ df)
162-
, -- , ("includePaths", text . show $ includePaths df)
163-
-- ("packageEnv", ppr $ packageEnv df)
164-
("pkgNames", vcat . map text $ pkgNames df)
90+
, ("pkgNames", vcat . map text $ pkgNames df)
16591
, ("packageFlags", vcat . map ppr $ packageFlags df)
166-
-- ,("pkgDatabase",(map) (ppr . installedPackageId) . pkgDatabase $ df)
167-
-- ("pkgDatabase", text . show <$> pkgDatabase $ df)
16892
]
16993

17094
vList :: [String] -> SDoc

plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Handlers.hs

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -372,31 +372,40 @@ type TEnv = String
372372
-- |GHC declarations required for expression evaluation
373373
evalSetup :: Ghc ()
374374
evalSetup = do
375-
preludeAsP <- parseImportDecl "import qualified Prelude as P"
375+
preludeAsP <- parseImportDecl "import qualified Prelude as P"
376376
-- 'myExecStmt' redirects the interpreted @stdout@ and @stderr@ to a temporary
377377
-- file in order to capture output produced as a side effect of evaluating a
378378
-- statement. The setup and teardown statements it injects need these modules
379379
-- in scope.
380-
systemIO <- parseImportDecl "import qualified System.IO"
380+
systemIO <- parseImportDecl "import qualified System.IO"
381381
ghcIOHandle <- parseImportDecl "import qualified GHC.IO.Handle"
382-
context <- getContext
382+
context <- getContext
383383
setContext (IIDecl preludeAsP : IIDecl systemIO : IIDecl ghcIOHandle : context)
384384

385385
-- | Evaluate every 'EvalExpr' and produce the 'TextEdit's that write the results
386386
-- back into the document, prefixing/padding each result line as the section's
387387
-- format requires.
388-
runEvalExprs :: Recorder (WithPriority Log) -> EvalConfig -> TEnv -> [(Section, EvalExpr)] -> Ghc [TextEdit]
388+
runEvalExprs ::
389+
Recorder (WithPriority Log)
390+
-> EvalConfig
391+
-> TEnv
392+
-> [(Section, EvalExpr)]
393+
-> Ghc [TextEdit]
389394
runEvalExprs recorder EvalConfig{..} e evalExprs = do
390395
df <- getInteractiveDynFlags
391396
evalSetup
392-
when (hasQuickCheck df && needsQuickCheck evalExprs) $ void $ evals recorder True e df propSetup
397+
when (hasQuickCheck df && needsQuickCheck evalExprs) $
398+
void $ evals recorder True e df propSetup
393399

394400
mapM (processEvalExpr e df) evalExprs
395401
where
396402
processEvalExpr :: TEnv -> DynFlags -> (Section, EvalExpr) -> Ghc TextEdit
397403
processEvalExpr fp df (section, evalExpr) = do
398404
let dbg = logWith recorder Debug
399-
let pad = pad_ $ (if isLiterate fp then ("> " `T.append`) else id) $ padPrefix (sectionFormat section)
405+
pre =
406+
(if isLiterate fp then ("> " `T.append`) else id) $
407+
padPrefix (sectionFormat section)
408+
pad = T.append pre
400409
rs <- runEvalExpr e df evalExpr
401410
dbg $ LogRunEvalExprResults rs
402411

@@ -406,13 +415,19 @@ runEvalExprs recorder EvalConfig{..} e evalExprs = do
406415
dbg $ LogRunEvalExprEdits edit
407416
return edit
408417

409-
-- runEvalExpr :: String -> DynFlags -> Loc EvalExpr -> Ghc [Text]
410-
runEvalExpr _ df evalExpr
418+
runEvalExpr :: String -> DynFlags -> EvalExpr -> Ghc [Text]
419+
runEvalExpr e df evalExpr
411420
| not (hasQuickCheck df) && isProperty evalExpr =
412421
return $
413422
singleLine
414423
"Add QuickCheck to your cabal dependencies to run this property."
415-
runEvalExpr e df evalExpr = evals recorder (eval_cfg_exception && not (isProperty evalExpr)) e df (asStatements evalExpr)
424+
| otherwise =
425+
evals
426+
recorder
427+
(eval_cfg_exception && not (isProperty evalExpr))
428+
e
429+
df
430+
(asStatements evalExpr)
416431

417432
-- | Build the edit that replaces the old result of an 'EvalExpr' with
418433
-- @resultLines@. For an 'EvalExpr' that sits on the closing @-}@ line of a
@@ -608,13 +623,6 @@ exceptionLines = (ix 0 %~ ("*** Exception: " <>)) . errorLines
608623
>>> map (pad_ (T.pack "--")) (map T.pack ["2+2",""])
609624
["--2+2","--<BLANKLINE>"]
610625
-}
611-
pad_ :: Text -> Text -> Text
612-
pad_ prefix = (prefix `T.append`) . convertBlank
613-
614-
convertBlank :: Text -> Text
615-
convertBlank x
616-
| T.null x = "<BLANKLINE>"
617-
| otherwise = x
618626

619627
padPrefix :: IsString p => Format -> p
620628
padPrefix SingleLine = "-- "

plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,17 +156,17 @@ hasPropertyEvalExpr = any isProperty . sectionEvalExprs
156156
splitSections :: [Section] -> ([Section], [Section])
157157
splitSections = partition ((== "setup") . sectionName)
158158

159-
data EvalExpr
160-
= Example {
159+
data EvalExpr =
160+
Example {
161161
evalExprLines :: NonEmpty Txt
162162
, evalExprOutput :: [Txt]
163163
, evalExprRange :: Range
164164
}
165165
| Property {
166-
evalExprLine :: Txt
167-
, evalExprOutput :: [Txt]
168-
, evalExprRange :: Range
169-
}
166+
evalExprLine :: Txt
167+
, evalExprOutput :: [Txt]
168+
, evalExprRange :: Range
169+
}
170170
deriving (Eq, Show, Generic, FromJSON, ToJSON, NFData)
171171

172172
data IsEvaluating = IsEvaluating

0 commit comments

Comments
 (0)