hnix-0.17.0: main/Repl.hs
{- 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 MultiWayIf #-}
module Repl
( main
, main'
) where
import Nix.Prelude hiding ( state )
import Nix hiding ( exec )
import Nix.Scope
import Nix.Value.Monad ( demand )
import qualified Data.HashMap.Lazy as M
import Data.Char ( isSpace )
import Data.List ( dropWhileEnd )
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import Data.Version ( showVersion )
import Paths_hnix ( version )
import Control.Monad.Catch
import Prettyprinter ( Doc
, space
)
import qualified Prettyprinter
import qualified Prettyprinter.Render.Text as Prettyprinter
import System.Console.Haskeline.Completion
( Completion(isFinished)
, completeWordWithPrev
, simpleCompletion
, listFiles
)
import System.Console.Repline ( Cmd
, CompletionFunc
, CompleterStyle(Prefix)
, MultiLine(SingleLine, MultiLine)
, ExitDecision(Exit)
, HaskelineT
, evalRepl
)
import qualified System.Console.Repline as Console
import qualified System.Exit as Exit
import qualified System.IO.Error as 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 =
do
s <- initState iniVal
evalStateT
(evalRepl
banner
(cmd . fromString)
options
(pure commandPrefix)
(pure "paste")
completion
(rcFile *> greeter)
finalizer
)
s
where
commandPrefix = ':'
banner = pure . \case
SingleLine -> "hnix> "
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 $ readFile ".hnixrc" `catch` handleMissing
traverse_
(\case
(prefixedCommand : xs) | Text.head prefixedCommand == commandPrefix ->
do
let
arguments = unwords xs
command = Text.tail prefixedCommand
optMatcher command options arguments
x -> cmd $ unwords x
)
(words <$> lines f)
handleMissing e
| Error.isDoesNotExistError e = stub
| otherwise = throwM 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
=> Text
-> Console.Options m
-> Text
-> m ()
optMatcher s [] _ = liftIO $ Text.putStrLn $ "No such command :" <> s
optMatcher s ((x, m) : xs) args
| s `Text.isPrefixOf` fromString x = m $ toString args
| otherwise = optMatcher s xs args
-- * Types
data IState t f m = IState
{ replIt :: Maybe NExprLoc -- ^ Last expression entered
, replCtx :: Scope (NValue t f m) -- ^ Scope. 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"
let
scope = coerce $
M.fromList $
("builtins", builtins) : fmap ("input",) (maybeToList mIni)
opts <- askOptions
pure $
IState
Nothing
scope
defReplConfig
{ cfgStrict = isStrict opts
, cfgValues = isValues opts
}
where
evalText :: (MonadNix e t f m) => Text -> m (NValue t f m)
evalText expr =
either
(\ e -> fail $ toString $ "Impossible happened: Unable to parse expression - '" <> expr <> "' fail was " <> show e)
evalExprLoc
(parseNixTextLoc expr)
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
state <- get
when (cfgDebug $ replCfg state) $ liftIO $ print state
-- Parser ( returns AST as `NExprLoc` )
case parseExprOrBinding source of
(Left err, _) ->
do
liftIO $ print err
pure Nothing
(Right expr, isBinding) ->
do
-- Type Inference ( returns Typing Environment )
--
-- import qualified Nix.Type.Env as Env
-- import Nix.Type.Infer
--
-- let tyctx' = inferTop mempty [("repl", stripAnnotation expr)]
-- liftIO $ print tyctx'
mVal <- lift $ lift $ try $ pushScope (replCtx state) (evalExprLoc expr)
either
(\ (NixException frames) ->
do
lift $ lift $ liftIO . print =<< renderFrames @(NValue t f m) @t frames
pure Nothing
)
(\ val ->
do
-- Update the interpreter state
when (update && isBinding) $ do
-- Set `replIt` to last entered expression
put state { replIt = pure expr }
-- If the result value is a set, update our context with it
case val of
NVSet _ (coerce -> scope) -> put state { replCtx = scope <> replCtx state }
_ -> stub
pure $ pure val
)
mVal
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 =
either
(\ e ->
either
(const (Left e, False)) -- return the first parsing failure
(\ e' -> (pure e', True))
(parseNixTextLoc $ toAttrSet i))
(\ expr -> (pure expr, False))
(parseNixTextLoc i)
toAttrSet i =
"{" <> i <> whenFalse ";" (Text.isSuffixOf ";" i) <> "}"
cmd
:: (MonadNix e t f m, MonadIO m)
=> Text
-> Repl e t f m ()
cmd source =
do
mVal <- exec True source
maybe
stub
printValue
mVal
printValue :: (MonadNix e t f m, MonadIO m)
=> NValue t f m
-> Repl e t f m ()
printValue val = do
cfg <- replCfg <$> get
let
g :: MonadIO m => Doc ann0 -> m ()
g = liftIO . print
lift $ lift $
(if
| cfgStrict cfg -> g . prettyNValue <=< normalForm
| cfgValues cfg -> g . prettyNValueProv <=< removeEffects
| otherwise -> g . prettyNValue <=< removeEffects
) val
-- * Commands
-- | @:browse@ command
browse :: (MonadNix e t f m, MonadIO m)
=> Text
-> Repl e t f m ()
browse _ =
do
state <- get
traverse_
(\(k, v) ->
do
liftIO $ Text.putStr $ coerce k <> " = "
printValue v
)
(M.toList $ coerce $ replCtx state)
-- | @:load@ command
load
:: (MonadNix e t f m, MonadIO m)
=> Path
-> Repl e t f m ()
load path =
do
contents <- liftIO $ readFile $
trim path
void $ exec True contents
where
trim :: Path -> Path
trim = coerce . dropWhileEnd isSpace . dropWhile isSpace . coerce
-- | @:type@ command
typeof
:: (MonadNix e t f m, MonadIO m)
=> Text
-> Repl e t f m ()
typeof src = do
state <- get
mVal <-
maybe
(exec False src)
(pure . pure)
(M.lookup (coerce src) (coerce $ replCtx state))
traverse_ printValueType mVal
where
printValueType = liftIO . Text.putStrLn <=< lift . lift . showValueType
-- | @:quit@ command
quit :: (MonadNix e t f m, MonadIO m) => a -> Repl e t f m ()
quit _ = liftIO Exit.exitSuccess
-- | @:set@ command
setConfig :: (MonadNix e t f m, MonadIO m) => Text -> Repl e t f m ()
setConfig args =
handlePresence
(liftIO $ Text.putStrLn "No option to set specified")
(\ (x:_xs) ->
case filter ((==x) . helpSetOptionName) helpSetOptions of
[opt] -> modify (\s -> s { replCfg = helpSetOptionFunction opt (replCfg s) })
_ -> liftIO $ Text.putStrLn "No such option"
)
$ words args
-- * Interactive Shell
-- | Prefix tab completer
defaultMatcher :: MonadIO m => [(String, CompletionFunc m)]
defaultMatcher =
one (":load", Console.fileCompleter)
completion
:: (MonadNix e t f m, MonadIO m)
=> CompleterStyle (StateT (IState t f m) m)
completion =
System.Console.Repline.Prefix
(completeWordWithPrev (pure '\\') 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)
-- 2021-04-02: So far conversiton to Text here is not productive,
-- since Haskeline uses String of all this.
=> String
-> String
-> (StateT (IState t f m) m) [Completion]
completeFunc reversedPrev word
-- Commands
| reversedPrev == ":" =
pure . listCompletion $
toString . helpOptionName <$>
(helpOptions :: HelpOptions e t f m)
-- Files
| any (`isPrefixOf` word) [ "/", "./", "../", "~/" ] =
listFiles word
-- Attributes of sets in REPL context
| var : subFields <- Text.split (== '.') (fromString word) , isPresent subFields =
do
state <- get
maybe
stub
(\ binding ->
do
candidates <- lift $ algebraicComplete subFields binding
pure $
notFinished <$>
listCompletion
(toString . (var <>) <$>
candidates
)
)
(M.lookup (coerce var) $ coerce $ replCtx state)
-- Builtins, context variables
| otherwise =
do
state <- get
let
scopeHashMap :: HashMap VarName (NValue t f m)
scopeHashMap = coerce $ replCtx state
contextKeys :: [VarName]
contextKeys = M.keys scopeHashMap
builtins :: AttrSet (NValue t f m)
(Just (NVSet _ builtins)) = M.lookup "builtins" scopeHashMap
shortBuiltins :: [VarName]
shortBuiltins = M.keys builtins
pure $ listCompletion $ toString <$>
one "__includes"
<> contextKeys
<> shortBuiltins
where
listCompletion = fmap simpleCompletion . filter (word `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 ("." <>) . M.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 ->
maybe
stub
((<<$>>)
(("." <> f) <>)
. algebraicComplete fs <=< demand
)
(M.lookup (coerce f) m)
in
case val of
NVSet _ xs -> withMap (M.mapKeys coerce xs)
_ -> stub
-- | HelpOption inspired by Dhall Repl
-- with `Doc` instead of String for syntax and doc
data HelpOption e t f m = HelpOption
{ helpOptionName :: Text
, 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"
mempty
"Print help text"
(help helpOptions . fromString)
, HelpOption
"paste"
mempty
"Enter multi-line mode"
(error "Unreachable")
, HelpOption
"load"
"FILENAME"
"Load .nix file into scope"
(load . fromString)
, HelpOption
"browse"
mempty
"Browse bindings in interpreter context"
(browse . fromString)
, HelpOption
"type"
"EXPRESSION"
"Evaluate expression or binding from context and print the type of the result value"
(typeof . fromString)
, HelpOption
"quit"
mempty
"Quit interpreter"
quit
, HelpOption
"set"
mempty
("Set REPL option"
<> Prettyprinter.line
<> "Available options:"
<> Prettyprinter.line
<> renderSetOptions helpSetOptions
)
(setConfig . fromString)
]
-- | Options for :set
data HelpSetOption = HelpSetOption
{ helpSetOptionName :: Text
, helpSetOptionSyntax :: Doc ()
, helpSetOptionDoc :: Doc ()
, helpSetOptionFunction :: ReplConfig -> ReplConfig
}
helpSetOptions :: [HelpSetOption]
helpSetOptions =
[ HelpSetOption
"strict"
mempty
"Enable strict evaluation of REPL expressions"
(\x -> x { cfgStrict = True})
, HelpSetOption
"lazy"
mempty
"Disable strict evaluation of REPL expressions"
(\x -> x { cfgStrict = False})
, HelpSetOption
"values"
mempty
"Enable printing of value provenance information"
(\x -> x { cfgValues = True})
, HelpSetOption
"novalues"
mempty
"Disable printing of value provenance information"
(\x -> x { cfgValues = False})
, HelpSetOption
"debug"
mempty
"Enable printing of REPL debug information"
(\x -> x { cfgDebug = True})
, HelpSetOption
"nodebug"
mempty
"Disable REPL debugging"
(\x -> x { cfgDebug = False})
]
renderSetOptions :: [HelpSetOption] -> Doc ()
renderSetOptions so =
Prettyprinter.indent 4 $
Prettyprinter.vsep $
(\h ->
Prettyprinter.pretty (helpSetOptionName h) <> space
<> helpSetOptionSyntax h
<> Prettyprinter.line
<> Prettyprinter.indent 4 (helpSetOptionDoc h)
) <$> so
help :: (MonadNix e t f m, MonadIO m)
=> HelpOptions e t f m
-> Text
-> Repl e t f m ()
help hs _ = do
liftIO $ putStrLn "Available commands:\n"
traverse_
(\h ->
liftIO .
Text.putStrLn .
Prettyprinter.renderStrict .
Prettyprinter.layoutPretty Prettyprinter.defaultLayoutOptions $
":"
<> Prettyprinter.pretty (helpOptionName h) <> space
<> helpOptionSyntax h
<> Prettyprinter.line
<> Prettyprinter.indent 4 (helpOptionDoc h)
)
hs
options
:: (MonadNix e t f m, MonadIO m)
=> Console.Options (Repl e t f m)
options = (\ h -> (toString $ helpOptionName h, helpOptionFunction h)) <$> helpOptions