hnix/main/Repl.hs

540 lines
16 KiB
Haskell

{- This code was authored by:
Stephen Diehl
Kwang Yul Seo <kwangyul.seo@gmail.com>
It was made available under the MIT license. See the src/Nix/Type
directory for more details.
-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Repl
( main
, main'
) where
import Nix hiding ( exec
, try
)
import Nix.Scope
import Nix.Utils
import Nix.Value.Monad (demand)
import qualified Data.List
import qualified Data.Maybe
import qualified Data.HashMap.Lazy
import Data.Text (Text)
import qualified Data.Text
import qualified Data.Text.IO
import Data.Version ( showVersion )
import Paths_hnix ( version )
import Control.Monad.Catch
import Control.Monad.Identity
import Control.Monad.Reader
import Control.Monad.State.Strict
import Prettyprinter (Doc, (<+>))
import qualified Prettyprinter
import qualified Prettyprinter.Render.Text
import System.Console.Haskeline.Completion
( Completion(isFinished)
, completeWordWithPrev
, simpleCompletion
, listFiles
)
import System.Console.Repline ( Cmd
, CompletionFunc
, CompleterStyle (Prefix)
, ExitDecision(Exit)
, HaskelineT
)
import qualified System.Console.Repline
import qualified System.Exit
import qualified System.IO.Error
-- | Repl entry point
main :: (MonadNix e t f m, MonadIO m, MonadMask m) => m ()
main = main' Nothing
-- | Principled version allowing to pass initial value for context.
--
-- Passed value is stored in context with "input" key.
main' :: (MonadNix e t f m, MonadIO m, MonadMask m) => Maybe (NValue t f m) -> m ()
main' iniVal = initState iniVal >>= \s -> flip evalStateT s
$ System.Console.Repline.evalRepl
banner
cmd
options
(Just commandPrefix)
(Just "paste")
completion
(rcFile >> greeter)
finalizer
where
commandPrefix = ':'
banner = pure . \case
System.Console.Repline.SingleLine -> "hnix> "
System.Console.Repline.MultiLine -> "| "
greeter =
liftIO
$ putStrLn
$ "Welcome to hnix "
<> showVersion version
<> ". For help type :help\n"
finalizer = do
liftIO $ putStrLn "Goodbye."
pure Exit
rcFile = do
f <- liftIO $ Data.Text.IO.readFile ".hnixrc" `catch` handleMissing
forM_ (map (words . Data.Text.unpack) $ Data.Text.lines f) $ \case
((prefix:command) : xs) | prefix == commandPrefix -> do
let arguments = unwords xs
optMatcher command options arguments
x -> cmd $ unwords x
handleMissing e
| System.IO.Error.isDoesNotExistError e = pure ""
| otherwise = throwIO e
-- Replicated and slightly adjusted `optMatcher` from `System.Console.Repline`
-- which doesn't export it.
-- * @MonadIO m@ instead of @MonadHaskeline m@
-- * @putStrLn@ instead of @outputStrLn@
optMatcher :: MonadIO m
=> String
-> System.Console.Repline.Options m
-> String
-> m ()
optMatcher s [] _ = liftIO $ putStrLn $ "No such command :" ++ s
optMatcher s ((x, m) : xs) args
| s `Data.List.isPrefixOf` x = m args
| otherwise = optMatcher s xs args
-------------------------------------------------------------------------------
-- Types
-------------------------------------------------------------------------------
data IState t f m = IState
{ replIt :: Maybe NExprLoc -- ^ Last expression entered
, replCtx :: AttrSet (NValue t f m) -- ^ Value environment
, replCfg :: ReplConfig -- ^ REPL configuration
} deriving (Eq, Show)
data ReplConfig = ReplConfig
{ cfgDebug :: Bool
, cfgStrict :: Bool
, cfgValues :: Bool
} deriving (Eq, Show)
defReplConfig :: ReplConfig
defReplConfig = ReplConfig
{ cfgDebug = False
, cfgStrict = False
, cfgValues = False
}
-- | Create initial IState for REPL
initState :: MonadNix e t f m => Maybe (NValue t f m) -> m (IState t f m)
initState mIni = do
builtins <- evalText "builtins"
opts :: Nix.Options <- asks (view hasLens)
pure $ IState
Nothing
(Data.HashMap.Lazy.fromList
$ ("builtins", builtins) : fmap ("input",) (Data.Maybe.maybeToList mIni))
defReplConfig
{ cfgStrict = strict opts
, cfgValues = values opts
}
where
evalText :: (MonadNix e t f m) => Text -> m (NValue t f m)
evalText expr = case parseNixTextLoc expr of
Failure e -> error $ "Impossible happened: Unable to parse expression - '" ++ Data.Text.unpack expr ++ "' error was " ++ show e
Success e -> do evalExprLoc e
type Repl e t f m = HaskelineT (StateT (IState t f m) m)
-------------------------------------------------------------------------------
-- Execution
-------------------------------------------------------------------------------
exec
:: forall e t f m
. (MonadNix e t f m, MonadIO m)
=> Bool
-> Text
-> Repl e t f m (Maybe (NValue t f m))
exec update source = do
-- Get the current interpreter state
st <- get
when (cfgDebug $ replCfg st) $ liftIO $ print st
-- Parser ( returns AST as `NExprLoc` )
case parseExprOrBinding source of
(Failure err, _) -> do
liftIO $ print err
pure Nothing
(Success expr, isBinding) -> do
-- Type Inference ( returns Typing Environment )
--
-- import qualified Nix.Type.Env as Env
-- import Nix.Type.Infer
--
-- let tyctx' = inferTop Env.empty [("repl", stripAnnotation expr)]
-- liftIO $ print tyctx'
mVal <- lift $ lift $ try $ pushScope (replCtx st) (evalExprLoc expr)
case mVal of
Left (NixException frames) -> do
lift $ lift $ liftIO . print =<< renderFrames @(NValue t f m) @t frames
pure Nothing
Right val -> do
-- Update the interpreter state
when (update && isBinding) $ do
-- Set `replIt` to last entered expression
put st { replIt = Just expr }
-- If the result value is a set, update our context with it
case val of
NVSet xs _ -> put st { replCtx = Data.HashMap.Lazy.union xs (replCtx st) }
_ -> pure ()
pure $ Just val
where
-- If parsing fails, turn the input into singleton attribute set
-- and try again.
--
-- This allows us to handle assignments like @a = 42@
-- which get turned into @{ a = 42; }@
parseExprOrBinding i =
case parseNixTextLoc i of
Success expr -> (Success expr, False)
Failure e ->
case parseNixTextLoc $ toAttrSet i of
Failure _ -> (Failure e, False) -- return the first parsing failure
Success e' -> (Success e', True)
toAttrSet i = "{" <> i <> (if Data.Text.isSuffixOf ";" i then mempty else ";") <> "}"
cmd
:: (MonadNix e t f m, MonadIO m)
=> String
-> Repl e t f m ()
cmd source = do
mVal <- exec True (Data.Text.pack source)
case mVal of
Nothing -> pure ()
Just val -> printValue val
printValue :: (MonadNix e t f m, MonadIO m)
=> NValue t f m
-> Repl e t f m ()
printValue val = do
cfg <- replCfg <$> get
lift $ lift $ do
if
| cfgStrict cfg -> liftIO . print . prettyNValue =<< normalForm val
| cfgValues cfg -> liftIO . print . prettyNValueProv =<< removeEffects val
| otherwise -> liftIO . print . prettyNValue =<< removeEffects val
-------------------------------------------------------------------------------
-- Commands
-------------------------------------------------------------------------------
-- :browse command
browse :: (MonadNix e t f m, MonadIO m)
=> String
-> Repl e t f m ()
browse _ = do
st <- get
forM_ (Data.HashMap.Lazy.toList $ replCtx st) $ \(k, v) -> do
liftIO $ putStr $ Data.Text.unpack $ k <> " = "
printValue v
-- :load command
load
:: (MonadNix e t f m, MonadIO m)
=> String
-> Repl e t f m ()
load args = do
contents <- liftIO
$ Data.Text.IO.readFile
$ Data.Text.unpack
$ Data.Text.strip
$ Data.Text.pack args
void $ exec True contents
-- :type command
typeof
:: (MonadNix e t f m, MonadIO m)
=> String
-> Repl e t f m ()
typeof args = do
st <- get
mVal <- case Data.HashMap.Lazy.lookup line (replCtx st) of
Just val -> pure $ Just val
Nothing -> do
exec False line
forM_ mVal $ \val -> do
s <- lift . lift . showValueType $ val
liftIO $ putStrLn s
where line = Data.Text.pack args
-- :quit command
quit :: (MonadNix e t f m, MonadIO m) => a -> Repl e t f m ()
quit _ = liftIO System.Exit.exitSuccess
-- :set command
setConfig :: (MonadNix e t f m, MonadIO m) => String -> Repl e t f m ()
setConfig args = case words args of
[] -> liftIO $ putStrLn "No option to set specified"
(x:_xs) ->
case filter ((==x) . helpSetOptionName) helpSetOptions of
[opt] -> modify (\s -> s { replCfg = helpSetOptionFunction opt (replCfg s) })
_ -> liftIO $ putStrLn "No such option"
-------------------------------------------------------------------------------
-- Interactive Shell
-------------------------------------------------------------------------------
-- Prefix tab completer
defaultMatcher :: MonadIO m => [(String, CompletionFunc m)]
defaultMatcher =
[ (":load", System.Console.Repline.fileCompleter)
]
completion
:: (MonadNix e t f m, MonadIO m)
=> CompleterStyle (StateT (IState t f m) m)
completion = System.Console.Repline.Prefix
(completeWordWithPrev (Just '\\') separators completeFunc)
defaultMatcher
where
separators :: String
separators = " \t[(,=+*&|}#?>:"
-- | Main completion function
--
-- Heavily inspired by Dhall Repl, with `algebraicComplete`
-- adjusted to monadic variant able to `demand` thunks.
completeFunc
:: forall e t f m . (MonadNix e t f m, MonadIO m)
=> String
-> String
-> (StateT (IState t f m) m) [Completion]
completeFunc reversedPrev word
-- Commands
| reversedPrev == ":"
= pure . listCompletion
$ map helpOptionName (helpOptions :: HelpOptions e t f m)
-- Files
| any (`Data.List.isPrefixOf` word) [ "/", "./", "../", "~/" ]
= listFiles word
-- Attributes of sets in REPL context
| var : subFields <- Data.Text.split (== '.') (Data.Text.pack word)
, not $ null subFields
= do
s <- get
case Data.HashMap.Lazy.lookup var (replCtx s) of
Nothing -> pure []
Just binding -> do
candidates <- lift $ algebraicComplete subFields binding
pure
$ map notFinished
$ listCompletion (Data.Text.unpack . (var <>) <$> candidates)
-- Builtins, context variables
| otherwise
= do
s <- get
let contextKeys = Data.HashMap.Lazy.keys (replCtx s)
(Just (NVSet builtins _)) = Data.HashMap.Lazy.lookup "builtins" (replCtx s)
shortBuiltins = Data.HashMap.Lazy.keys builtins
pure $ listCompletion
$ ["__includes"]
++ (Data.Text.unpack <$> contextKeys)
++ (Data.Text.unpack <$> shortBuiltins)
where
listCompletion = map simpleCompletion . filter (word `Data.List.isPrefixOf`)
notFinished x = x { isFinished = False }
algebraicComplete :: (MonadNix e t f m)
=> [Text]
-> NValue t f m
-> m [Text]
algebraicComplete subFields val =
let keys = fmap ("." <>) . Data.HashMap.Lazy.keys
withMap m =
case subFields of
[] -> pure $ keys m
-- Stop on last subField (we care about the keys at this level)
[_] -> pure $ keys m
f:fs ->
case Data.HashMap.Lazy.lookup f m of
Nothing -> pure []
Just e ->
demand e
(\e' -> fmap (("." <> f) <>) <$> algebraicComplete fs e')
in case val of
NVSet xs _ -> withMap xs
_ -> pure []
-- HelpOption inspired by Dhall Repl
-- with `Doc` instead of String for syntax and doc
data HelpOption e t f m = HelpOption
{ helpOptionName :: String
, helpOptionSyntax :: Doc ()
, helpOptionDoc :: Doc ()
, helpOptionFunction :: Cmd (Repl e t f m)
}
type HelpOptions e t f m = [HelpOption e t f m]
helpOptions :: (MonadNix e t f m, MonadIO m) => HelpOptions e t f m
helpOptions =
[ HelpOption
"help"
""
"Print help text"
(help helpOptions)
, HelpOption
"paste"
""
"Enter multi-line mode"
(error "Unreachable")
, HelpOption
"load"
"FILENAME"
"Load .nix file into scope"
load
, HelpOption
"browse"
""
"Browse bindings in interpreter context"
browse
, HelpOption
"type"
"EXPRESSION"
"Evaluate expression or binding from context and print the type of the result value"
typeof
, HelpOption
"quit"
""
"Quit interpreter"
quit
, HelpOption
"set"
""
( "Set REPL option"
<> Prettyprinter.line
<> "Available options:"
<> Prettyprinter.line
<> renderSetOptions helpSetOptions
)
setConfig
]
-- Options for :set
data HelpSetOption = HelpSetOption
{ helpSetOptionName :: String
, helpSetOptionSyntax :: Doc ()
, helpSetOptionDoc :: Doc ()
, helpSetOptionFunction :: ReplConfig -> ReplConfig
}
helpSetOptions :: [HelpSetOption]
helpSetOptions =
[ HelpSetOption
"strict"
""
"Enable strict evaluation of REPL expressions"
(\x -> x { cfgStrict = True})
, HelpSetOption
"lazy"
""
"Disable strict evaluation of REPL expressions"
(\x -> x { cfgStrict = False})
, HelpSetOption
"values"
""
"Enable printing of value provenance information"
(\x -> x { cfgValues = True})
, HelpSetOption
"novalues"
""
"Disable printing of value provenance information"
(\x -> x { cfgValues = False})
, HelpSetOption
"debug"
""
"Enable printing of REPL debug information"
(\x -> x { cfgDebug = True})
, HelpSetOption
"nodebug"
""
"Disable REPL debugging"
(\x -> x { cfgDebug = False})
]
renderSetOptions :: [HelpSetOption] -> Doc ()
renderSetOptions so =
Prettyprinter.indent 4
$ Prettyprinter.vsep
$ flip map so
$ \h ->
Prettyprinter.pretty (helpSetOptionName h)
<+> helpSetOptionSyntax h
<> Prettyprinter.line
<> Prettyprinter.indent 4 (helpSetOptionDoc h)
help :: (MonadNix e t f m, MonadIO m)
=> HelpOptions e t f m
-> String
-> Repl e t f m ()
help hs _ = do
liftIO $ putStrLn "Available commands:\n"
forM_ hs $ \h ->
liftIO
. Data.Text.IO.putStrLn
. Prettyprinter.Render.Text.renderStrict
. Prettyprinter.layoutPretty
Prettyprinter.defaultLayoutOptions
$ ":"
<> Prettyprinter.pretty (helpOptionName h)
<+> helpOptionSyntax h
<> Prettyprinter.line
<> Prettyprinter.indent 4 (helpOptionDoc h)
options
:: (MonadNix e t f m, MonadIO m)
=> System.Console.Repline.Options (Repl e t f m)
options = (\h -> (helpOptionName h, helpOptionFunction h)) <$> helpOptions