From 710bc843098d4731d569426b257c65dc5c3ef7a1 Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Fri, 26 Jun 2020 12:06:45 +0200 Subject: [PATCH 01/11] Support repline 0.4 Closes #651. `haskeline` override needs `dontCheck` as it tries to run an executable. Tested build and repl itself. --- hnix.cabal | 2 +- main/Repl.hs | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/hnix.cabal b/hnix.cabal index 1a4a35b..03075da 100644 --- a/hnix.cabal +++ b/hnix.cabal @@ -958,7 +958,7 @@ executable hnix , pretty-show , prettyprinter , ref-tf - , repline + , repline >= 0.4.0.0 && < 0.5 , serialise , template-haskell , text diff --git a/main/Repl.hs b/main/Repl.hs index ed9b4b2..708e707 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -58,7 +58,15 @@ import System.Exit main :: (MonadNix e t f m, MonadIO m, MonadMask m) => m () main = flip evalStateT initState - $ evalRepl (return prefix) cmd options (Just ':') completer welcomeText + $ evalRepl + (const $ return prefix) + cmd + options + (Just ':') + Nothing + completer + welcomeText + (return Exit) where prefix = "hnix> " welcomeText = @@ -139,7 +147,7 @@ cmd source = do ------------------------------------------------------------------------------- -- :browse command -browse :: MonadNix e t f m => [String] -> Repl e t f m () +browse :: MonadNix e t f m => String -> Repl e t f m () browse _ = do st <- get undefined @@ -148,16 +156,16 @@ browse _ = do -- :load command load :: (MonadNix e t f m, MonadIO m) - => [String] + => String -> Repl e t f m () load args = do - contents <- liftIO $ Text.readFile (unwords args) + contents <- liftIO $ Text.readFile args void $ exec True contents -- :type command typeof :: (MonadNix e t f m, MonadIO m) - => [String] + => String -> Repl e t f m () typeof args = do st <- get @@ -166,7 +174,7 @@ typeof args = do Nothing -> exec False line str <- lift $ lift $ showValueType val liftIO $ putStrLn str - where line = Text.pack (unwords args) + where line = Text.pack args -- :quit command quit :: (MonadNix e t f m, MonadIO m) => a -> Repl e t f m () @@ -194,11 +202,9 @@ comp n = do options :: (MonadNix e t f m, MonadIO m) - => [(String, [String] -> Repl e t f m ())] + => System.Console.Repline.Options (Repl e t f m) options = - [ ( "load" - , load - ) + [ ( "load" , load) --, ("browse" , browse) , ("quit", quit) , ("type", typeof) @@ -208,7 +214,7 @@ options = help :: forall e t f m . (MonadNix e t f m, MonadIO m) - => [String] + => String -> Repl e t f m () help _ = liftIO $ do putStrLn "Available commands:\n" From 59e65d094a19c8450ed470d9d2814d5bb222444f Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Sun, 28 Jun 2020 11:25:42 +0200 Subject: [PATCH 02/11] repl: support multi-line input ``` Welcome to hnix 0.9.0. For help type :help hnix> :paste -- Entering multi-line mode. Press to finish. | { just | = | '' | testing | ''; | } | { just = ""; } hnix> Goodbye. ``` Closes #665. --- main/Repl.hs | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/main/Repl.hs b/main/Repl.hs index 708e707..61cb476 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -7,6 +7,7 @@ directory for more details. -} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiWayIf #-} @@ -51,30 +52,40 @@ import Control.Monad.Identity import Control.Monad.Reader import Control.Monad.State.Strict -import System.Console.Repline hiding ( options, prefix ) -import System.Environment -import System.Exit +import System.Console.Repline ( CompletionFunc + , CompleterStyle (Prefix) + , ExitDecision(Exit) + , HaskelineT + , WordCompleter + ) +import qualified System.Console.Repline +import qualified System.Exit main :: (MonadNix e t f m, MonadIO m, MonadMask m) => m () main = flip evalStateT initState - $ evalRepl - (const $ return prefix) + $ System.Console.Repline.evalRepl + banner cmd options (Just ':') - Nothing + (Just "paste") completer - welcomeText - (return Exit) + greeter + finalizer where - prefix = "hnix> " - welcomeText = + 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." + return Exit ------------------------------------------------------------------------------- -- Types @@ -92,7 +103,7 @@ hoistErr :: (MonadIO m, MonadThrow m) => Result a -> Repl e t f m a hoistErr (Success val) = return val hoistErr (Failure err) = do liftIO $ print err - abort + System.Console.Repline.abort ------------------------------------------------------------------------------- -- Execution @@ -121,7 +132,7 @@ exec update source = do case mVal of Left (NixException frames) -> do lift $ lift $ liftIO . print =<< renderFrames @(NValue t f m) @t frames - abort + System.Console.Repline.abort Right val -> do -- Update the interpreter state when update $ do @@ -178,7 +189,7 @@ typeof args = do -- :quit command quit :: (MonadNix e t f m, MonadIO m) => a -> Repl e t f m () -quit _ = liftIO exitSuccess +quit _ = liftIO System.Exit.exitSuccess ------------------------------------------------------------------------------- -- Interactive Shell @@ -187,7 +198,7 @@ quit _ = liftIO exitSuccess -- Prefix tab completer defaultMatcher :: MonadIO m => [(String, CompletionFunc m)] defaultMatcher = - [(":load", fileCompleter) + [(":load", System.Console.Repline.fileCompleter) --, (":type" , values) ] @@ -219,8 +230,9 @@ help help _ = liftIO $ do putStrLn "Available commands:\n" mapM_ putStrLn $ map (\o -> ":" ++ (fst o)) (options @e @t @f @m) + putStrLn ":paste - enter multi-line mode" completer :: (MonadNix e t f m, MonadIO m) => CompleterStyle (StateT (IState t f m) m) -completer = Prefix (wordCompleter comp) defaultMatcher +completer = Prefix (System.Console.Repline.wordCompleter comp) defaultMatcher From d9303f385c916913cf123998d4c1799b66c091af Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Tue, 30 Jun 2020 10:33:11 +0200 Subject: [PATCH 03/11] repl: Refactor error handling Drops Repline `abort`s and switches to `Maybe` result for `call` function. Few other bits handled too: - `M.empty` -> `mempty` - qualified use of `Data.Text` - minor formatting fixes for readability Closes #505. --- main/Repl.hs | 104 ++++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/main/Repl.hs b/main/Repl.hs index 61cb476..00501f4 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -39,11 +39,9 @@ import Data.List ( isPrefixOf , foldl' ) import qualified Data.Map as Map -import Data.Text ( unpack - , pack - ) -import qualified Data.Text as Text -import qualified Data.Text.IO as Text +import Data.Text (Text) +import qualified Data.Text +import qualified Data.Text.IO import Data.Version ( showVersion ) import Paths_hnix ( version ) @@ -96,14 +94,9 @@ newtype IState t f m = IState } initState :: MonadIO m => IState t f m -initState = IState M.empty +initState = IState mempty type Repl e t f m = HaskelineT (StateT (IState t f m) m) -hoistErr :: (MonadIO m, MonadThrow m) => Result a -> Repl e t f m a -hoistErr (Success val) = return val -hoistErr (Failure err) = do - liftIO $ print err - System.Console.Repline.abort ------------------------------------------------------------------------------- -- Execution @@ -113,46 +106,53 @@ exec :: forall e t f m . (MonadNix e t f m, MonadIO m) => Bool - -> Text.Text - -> Repl e t f m (NValue t f m) + -> Text + -> Repl e t f m (Maybe (NValue t f m)) exec update source = do -- Get the current interpreter state - st <- get + st <- get -- Parser ( returns AST ) -- TODO: parse = - expr <- hoistErr $ parseNixTextLoc source + case parseNixTextLoc source of + Failure err -> do + liftIO $ print err + return Nothing + Success expr -> do + -- Type Inference ( returns Typing Environment ) + --let tyctx' = inferTop Env.empty [("repl", stripAnnotation expr)] + --liftIO $ print tyctx' - -- Type Inference ( returns Typing Environment ) - -- tyctx' <- hoistErr $ inferTop (tyctx st) expr - - -- TODO: track scope with (tmctx st) - mVal <- lift $ lift $ try $ pushScope M.empty (evalExprLoc expr) - - case mVal of - Left (NixException frames) -> do - lift $ lift $ liftIO . print =<< renderFrames @(NValue t f m) @t frames - System.Console.Repline.abort - Right val -> do - -- Update the interpreter state - when update $ do - -- Create the new environment - put st { tmctx = tmctx st } -- TODO: M.insert key val (tmctx st) - return val + -- TODO: track scope with (tmctx st) + mVal <- lift $ lift $ try $ pushScope mempty (evalExprLoc expr) + case mVal of + Left (NixException frames) -> do + lift $ lift $ liftIO . print =<< renderFrames @(NValue t f m) @t frames + return Nothing + Right val -> do + -- Update the interpreter state + when update $ do + -- Create the new environment + put st { tmctx = tmctx st } -- TODO: M.insert key val (tmctx st) + return $ Just val cmd :: (MonadNix e t f m, MonadIO m) => String -> Repl e t f m () cmd source = do - val <- exec True (Text.pack source) - lift $ lift $ do - opts :: Nix.Options <- asks (view hasLens) - if - | strict opts -> liftIO . print . prettyNValue =<< normalForm val - | values opts -> liftIO . print . prettyNValueProv =<< removeEffects val - | otherwise -> liftIO . print . prettyNValue =<< removeEffects val + mVal <- exec True (Data.Text.pack source) + case mVal of + Nothing -> return () + Just val -> do + lift $ lift $ do + opts :: Nix.Options <- asks (view hasLens) + if + | strict opts -> liftIO . print . prettyNValue =<< normalForm val + | values opts -> liftIO . print . prettyNValueProv =<< removeEffects val + | otherwise -> liftIO . print . prettyNValue =<< removeEffects val + ------------------------------------------------------------------------------- -- Commands ------------------------------------------------------------------------------- @@ -170,7 +170,7 @@ load => String -> Repl e t f m () load args = do - contents <- liftIO $ Text.readFile args + contents <- liftIO $ Data.Text.IO.readFile args void $ exec True contents -- :type command @@ -179,13 +179,17 @@ typeof => String -> Repl e t f m () typeof args = do - st <- get - val <- case M.lookup line (tmctx st) of - Just val -> return val - Nothing -> exec False line - str <- lift $ lift $ showValueType val - liftIO $ putStrLn str - where line = Text.pack args + st <- get + mVal <- case M.lookup line (tmctx st) of + Just val -> return $ 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 () @@ -201,15 +205,15 @@ defaultMatcher = [(":load", System.Console.Repline.fileCompleter) --, (":type" , values) ] - -- Default tab completer comp :: Monad m => WordCompleter m comp n = do let cmds = [":load", ":type", ":browse", ":quit"] -- Env.TypeEnv ctx <- gets tyctx - -- let defs = map unpack $ Map.keys ctx - return $ filter (isPrefixOf n) (cmds {-++ defs-} - ) + -- let defs = map Data.Text.unpack $ Map.keys ctx + return $ filter (isPrefixOf n) (cmds + -- ++ defs + ) options :: (MonadNix e t f m, MonadIO m) From 0e8e73ec88343015008e5dc9d9f1deba65ba8980 Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Tue, 30 Jun 2020 12:10:59 +0200 Subject: [PATCH 04/11] repl: Extend IState, handle bindings, add browse and debug commands Bindings are now handled correctly and you can do ``` a = 2 b = 3 c = a + b :browse :t c ``` Converted IState to data type instead of `newtype`. Now tracks `replIt` for last entered expression that parsed successfully. `replCtx` is now extended with bindings and browseable via `:browse`. :`debug` can be used to enable dumping of `IState` on input. Scope taken from `replCtx` is now pushed to evaluated values. --- main/Repl.hs | 105 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 35 deletions(-) diff --git a/main/Repl.hs b/main/Repl.hs index 00501f4..60e36c3 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -34,12 +34,9 @@ import Nix.Type.Infer import Nix.Utils import Control.Comonad -import qualified Data.HashMap.Lazy as M -import Data.List ( isPrefixOf - , foldl' - ) -import qualified Data.Map as Map -import Data.Text (Text) +import qualified Data.List +import qualified Data.HashMap.Lazy +import Data.Text (Text) import qualified Data.Text import qualified Data.Text.IO import Data.Version ( showVersion ) @@ -89,12 +86,14 @@ main = flip evalStateT initState -- Types ------------------------------------------------------------------------------- -newtype IState t f m = IState - { tmctx :: AttrSet (NValue t f m) -- Value environment - } +data IState t f m = IState + { replIt :: Maybe NExprLoc -- ^ Last expression entered + , replCtx :: AttrSet (NValue t f m) -- ^ Value environment + , replDbg :: Bool -- ^ Enable REPL debug output, dumping IState on each command + } deriving (Eq, Show) initState :: MonadIO m => IState t f m -initState = IState mempty +initState = IState Nothing mempty False type Repl e t f m = HaskelineT (StateT (IState t f m) m) @@ -112,19 +111,21 @@ exec update source = do -- Get the current interpreter state st <- get - -- Parser ( returns AST ) - -- TODO: parse = - case parseNixTextLoc source of - Failure err -> do + when (replDbg st) $ liftIO $ print st + + -- Parser ( returns AST as `NExprLoc` ) + case parseExprOrBinding source of + (Failure err, _) -> do liftIO $ print err return Nothing - Success expr -> do + (Success expr, isBinding) -> do + -- Type Inference ( returns Typing Environment ) + -- --let tyctx' = inferTop Env.empty [("repl", stripAnnotation expr)] --liftIO $ print tyctx' - -- TODO: track scope with (tmctx st) - mVal <- lift $ lift $ try $ pushScope mempty (evalExprLoc expr) + mVal <- lift $ lift $ try $ pushScope (replCtx st) (evalExprLoc expr) case mVal of Left (NixException frames) -> do @@ -132,10 +133,31 @@ exec update source = do return Nothing Right val -> do -- Update the interpreter state - when update $ do - -- Create the new environment - put st { tmctx = tmctx st } -- TODO: M.insert key val (tmctx st) + 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) } + _ -> return () + return $ 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) @@ -145,24 +167,31 @@ cmd source = do mVal <- exec True (Data.Text.pack source) case mVal of Nothing -> return () - Just val -> do - lift $ lift $ do - opts :: Nix.Options <- asks (view hasLens) - if - | strict opts -> liftIO . print . prettyNValue =<< normalForm val - | values opts -> liftIO . print . prettyNValueProv =<< removeEffects val - | otherwise -> liftIO . print . prettyNValue =<< removeEffects val + Just val -> printValue val + +printValue :: (MonadNix e t f m, MonadIO m) + => NValue t f m + -> Repl e t f m () +printValue val = lift $ lift $ do + opts :: Nix.Options <- asks (view hasLens) + if + | strict opts -> liftIO . print . prettyNValue =<< normalForm val + | values opts -> liftIO . print . prettyNValueProv =<< removeEffects val + | otherwise -> liftIO . print . prettyNValue =<< removeEffects val ------------------------------------------------------------------------------- -- Commands ------------------------------------------------------------------------------- -- :browse command -browse :: MonadNix e t f m => String -> Repl e t f m () +browse :: (MonadNix e t f m, MonadIO m) + => String + -> Repl e t f m () browse _ = do st <- get - undefined - -- liftIO $ mapM_ putStrLn $ ppenv (tyctx st) + forM_ (Data.HashMap.Lazy.toList $ replCtx st) $ \(k, v) -> do + liftIO $ putStr $ Data.Text.unpack $ k <> " = " + printValue v -- :load command load @@ -180,7 +209,7 @@ typeof -> Repl e t f m () typeof args = do st <- get - mVal <- case M.lookup line (tmctx st) of + mVal <- case Data.HashMap.Lazy.lookup line (replCtx st) of Just val -> return $ Just val Nothing -> do exec False line @@ -195,6 +224,10 @@ typeof args = do quit :: (MonadNix e t f m, MonadIO m) => a -> Repl e t f m () quit _ = liftIO System.Exit.exitSuccess +-- :debug command +debug :: (MonadNix e t f m, MonadIO m) => a -> Repl e t f m () +debug _ = modify (\x -> x { replDbg = True }) + ------------------------------------------------------------------------------- -- Interactive Shell ------------------------------------------------------------------------------- @@ -202,16 +235,17 @@ quit _ = liftIO System.Exit.exitSuccess -- Prefix tab completer defaultMatcher :: MonadIO m => [(String, CompletionFunc m)] defaultMatcher = - [(":load", System.Console.Repline.fileCompleter) + [ (":load", System.Console.Repline.fileCompleter) --, (":type" , values) - ] + ] + -- Default tab completer comp :: Monad m => WordCompleter m comp n = do let cmds = [":load", ":type", ":browse", ":quit"] -- Env.TypeEnv ctx <- gets tyctx -- let defs = map Data.Text.unpack $ Map.keys ctx - return $ filter (isPrefixOf n) (cmds + return $ filter (Data.List.isPrefixOf n) (cmds -- ++ defs ) @@ -220,10 +254,11 @@ options => System.Console.Repline.Options (Repl e t f m) options = [ ( "load" , load) - --, ("browse" , browse) + , ("browse" , browse) , ("quit", quit) , ("type", typeof) , ("help", help) + , ("debug", debug) ] help From 4ce176d1a1327e10c6eda60cb1decd7e98d41605 Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Tue, 30 Jun 2020 13:07:27 +0200 Subject: [PATCH 05/11] repl: Better help using HelpOption type --- main/Repl.hs | 99 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/main/Repl.hs b/main/Repl.hs index 60e36c3..a947c84 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -39,6 +39,9 @@ import qualified Data.HashMap.Lazy import Data.Text (Text) import qualified Data.Text import qualified Data.Text.IO +import Data.Text.Prettyprint.Doc (Doc, (<+>)) +import qualified Data.Text.Prettyprint.Doc +import qualified Data.Text.Prettyprint.Doc.Render.Text import Data.Version ( showVersion ) import Paths_hnix ( version ) @@ -47,7 +50,8 @@ import Control.Monad.Identity import Control.Monad.Reader import Control.Monad.State.Strict -import System.Console.Repline ( CompletionFunc +import System.Console.Repline ( Cmd + , CompletionFunc , CompleterStyle (Prefix) , ExitDecision(Exit) , HaskelineT @@ -199,7 +203,11 @@ load => String -> Repl e t f m () load args = do - contents <- liftIO $ Data.Text.IO.readFile args + contents <- liftIO + $ Data.Text.IO.readFile + $ Data.Text.unpack + $ Data.Text.strip + $ Data.Text.pack args void $ exec True contents -- :type command @@ -249,27 +257,78 @@ comp n = do -- ++ defs ) +-- 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 + "debug" + "" + "Enable REPL debugging output" + debug + ] + +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 + . Data.Text.Prettyprint.Doc.Render.Text.renderStrict + . Data.Text.Prettyprint.Doc.layoutPretty + Data.Text.Prettyprint.Doc.defaultLayoutOptions + $ ":" + <> Data.Text.Prettyprint.Doc.pretty (helpOptionName h) + <+> helpOptionSyntax h + <> Data.Text.Prettyprint.Doc.line + <> Data.Text.Prettyprint.Doc.indent 4 (helpOptionDoc h) + options :: (MonadNix e t f m, MonadIO m) => System.Console.Repline.Options (Repl e t f m) -options = - [ ( "load" , load) - , ("browse" , browse) - , ("quit", quit) - , ("type", typeof) - , ("help", help) - , ("debug", debug) - ] - -help - :: forall e t f m - . (MonadNix e t f m, MonadIO m) - => String - -> Repl e t f m () -help _ = liftIO $ do - putStrLn "Available commands:\n" - mapM_ putStrLn $ map (\o -> ":" ++ (fst o)) (options @e @t @f @m) - putStrLn ":paste - enter multi-line mode" +options = (\h -> (helpOptionName h, helpOptionFunction h)) <$> helpOptions completer :: (MonadNix e t f m, MonadIO m) From f7704b4c280defb8249986b28dd73c640d7c810d Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Tue, 30 Jun 2020 14:13:47 +0200 Subject: [PATCH 06/11] repl: Pass result of --eval -E "..expr.." to REPL Allows us to do ``` hnix --eval -E '{ a = 2; b = "test"; }' --repl hnix> :browse input = { a = 2; b = "test"; } hnix> input.a 2 hnix> input.b "test" ``` Closes #292. --- main/Main.hs | 7 ++++++- main/Repl.hs | 23 ++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/main/Main.hs b/main/Main.hs index 50e90a7..05701c8 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -102,7 +102,12 @@ main = do @(StdThunk (StandardT (StdIdT IO))) frames - when (repl opts) $ withNixContext Nothing Repl.main + when (repl opts) $ + if evaluate opts + then do + val <- Nix.nixEvalExprLoc mpath expr + withNixContext Nothing (Repl.main' $ Just val) + else withNixContext Nothing Repl.main process opts mpath expr | evaluate opts diff --git a/main/Repl.hs b/main/Repl.hs index a947c84..230236b 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -20,7 +20,10 @@ {-# OPTIONS_GHC -Wno-unused-matches #-} {-# OPTIONS_GHC -Wno-unused-imports #-} -module Repl where +module Repl + ( main + , main' + ) where import Nix hiding ( exec , try @@ -60,9 +63,15 @@ import System.Console.Repline ( Cmd import qualified System.Console.Repline import qualified System.Exit +-- | Repl entry point +main :: (MonadNix e t f m, MonadIO m, MonadMask m) => m () +main = main' Nothing -main :: (MonadNix e t f m, MonadIO m, MonadMask m) => m () -main = flip evalStateT initState +-- | 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 = flip evalStateT (initState iniVal) $ System.Console.Repline.evalRepl banner cmd @@ -96,8 +105,12 @@ data IState t f m = IState , replDbg :: Bool -- ^ Enable REPL debug output, dumping IState on each command } deriving (Eq, Show) -initState :: MonadIO m => IState t f m -initState = IState Nothing mempty False +initState :: MonadIO m => Maybe (NValue t f m) -> IState t f m +initState mIni = + IState + Nothing + (maybe mempty (\x -> Data.HashMap.Lazy.fromList [("input", x)]) mIni) + False type Repl e t f m = HaskelineT (StateT (IState t f m) m) From b84f4f6c196ad711e549e132a0495e0ac0989dcf Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Tue, 30 Jun 2020 15:22:07 +0200 Subject: [PATCH 07/11] repl: Support loading .hnixrc from current directory --- .gitignore | 1 + main/Repl.hs | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6191459..8595428 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ ctags dist-newstyle result* .ghc.environment.* +.hnixrc diff --git a/main/Repl.hs b/main/Repl.hs index 230236b..5d055be 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -62,6 +62,7 @@ import System.Console.Repline ( Cmd ) 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 () @@ -76,15 +77,18 @@ main' iniVal = flip evalStateT (initState iniVal) banner cmd options - (Just ':') + (Just commandPrefix) (Just "paste") completer - greeter + (rcFile >> greeter) finalizer where + commandPrefix = ':' + banner = pure . \case System.Console.Repline.SingleLine -> "hnix> " System.Console.Repline.MultiLine -> "| " + greeter = liftIO $ putStrLn @@ -95,6 +99,32 @@ main' iniVal = flip evalStateT (initState iniVal) liftIO $ putStrLn "Goodbye." return 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 = return "" + | 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 ------------------------------------------------------------------------------- From 8d0e325cf5cb3d5c8aaa0c0f059d85a8343902d0 Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Thu, 2 Jul 2020 11:15:58 +0200 Subject: [PATCH 08/11] repl: Add ReplConfig and :set command --- main/Repl.hs | 134 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 19 deletions(-) diff --git a/main/Repl.hs b/main/Repl.hs index 5d055be..960c4c9 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -14,6 +14,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeSynonymInstances #-} @@ -38,6 +39,7 @@ import Nix.Utils import Control.Comonad import qualified Data.List +import qualified Data.Maybe import qualified Data.HashMap.Lazy import Data.Text (Text) import qualified Data.Text @@ -72,7 +74,7 @@ main = main' Nothing -- -- 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 = flip evalStateT (initState iniVal) +main' iniVal = initState iniVal >>= \s -> flip evalStateT s $ System.Console.Repline.evalRepl banner cmd @@ -132,15 +134,45 @@ main' iniVal = flip evalStateT (initState iniVal) data IState t f m = IState { replIt :: Maybe NExprLoc -- ^ Last expression entered , replCtx :: AttrSet (NValue t f m) -- ^ Value environment - , replDbg :: Bool -- ^ Enable REPL debug output, dumping IState on each command + , replCfg :: ReplConfig -- ^ REPL configuration } deriving (Eq, Show) -initState :: MonadIO m => Maybe (NValue t f m) -> IState t f m -initState mIni = - IState +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 - (maybe mempty (\x -> Data.HashMap.Lazy.fromList [("input", x)]) mIni) - False + (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 + value <- evalExprLoc e + pure value type Repl e t f m = HaskelineT (StateT (IState t f m) m) @@ -158,7 +190,7 @@ exec update source = do -- Get the current interpreter state st <- get - when (replDbg st) $ liftIO $ print st + when (cfgDebug $ replCfg st) $ liftIO $ print st -- Parser ( returns AST as `NExprLoc` ) case parseExprOrBinding source of @@ -219,12 +251,13 @@ cmd source = do printValue :: (MonadNix e t f m, MonadIO m) => NValue t f m -> Repl e t f m () -printValue val = lift $ lift $ do - opts :: Nix.Options <- asks (view hasLens) - if - | strict opts -> liftIO . print . prettyNValue =<< normalForm val - | values opts -> liftIO . print . prettyNValueProv =<< removeEffects val - | otherwise -> liftIO . print . prettyNValue =<< removeEffects val +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 @@ -275,9 +308,14 @@ typeof args = do quit :: (MonadNix e t f m, MonadIO m) => a -> Repl e t f m () quit _ = liftIO System.Exit.exitSuccess --- :debug command -debug :: (MonadNix e t f m, MonadIO m) => a -> Repl e t f m () -debug _ = modify (\x -> x { replDbg = True }) +-- :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 @@ -344,12 +382,70 @@ helpOptions = "Quit interpreter" quit , HelpOption + "set" + "" + ( "Set REPL option" + <> Data.Text.Prettyprint.Doc.line + <> "Available options:" + <> Data.Text.Prettyprint.Doc.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 REPL debugging output" - 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 = + Data.Text.Prettyprint.Doc.indent 4 + $ Data.Text.Prettyprint.Doc.vsep + $ flip map so + $ \h -> + Data.Text.Prettyprint.Doc.pretty (helpSetOptionName h) + <+> helpSetOptionSyntax h + <> Data.Text.Prettyprint.Doc.line + <> Data.Text.Prettyprint.Doc.indent 4 (helpSetOptionDoc h) + help :: (MonadNix e t f m, MonadIO m) => HelpOptions e t f m -> String From f964284c6660a0c0bcc7556d6c66f54a9157c660 Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Thu, 2 Jul 2020 11:41:15 +0200 Subject: [PATCH 09/11] repl: improve tab completion --- main/Repl.hs | 105 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 16 deletions(-) diff --git a/main/Repl.hs b/main/Repl.hs index 960c4c9..2958f7d 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -36,6 +36,7 @@ import Nix.Scope import qualified Nix.Type.Env as Env import Nix.Type.Infer import Nix.Utils +import Nix.Value.Monad (demand) import Control.Comonad import qualified Data.List @@ -55,6 +56,12 @@ import Control.Monad.Identity import Control.Monad.Reader import Control.Monad.State.Strict +import System.Console.Haskeline.Completion + ( Completion(isFinished) + , completeWordWithPrev + , simpleCompletion + , listFiles + ) import System.Console.Repline ( Cmd , CompletionFunc , CompleterStyle (Prefix) @@ -81,7 +88,7 @@ main' iniVal = initState iniVal >>= \s -> flip evalStateT s options (Just commandPrefix) (Just "paste") - completer + completion (rcFile >> greeter) finalizer where @@ -325,18 +332,89 @@ setConfig args = case words args of defaultMatcher :: MonadIO m => [(String, CompletionFunc m)] defaultMatcher = [ (":load", System.Console.Repline.fileCompleter) - --, (":type" , values) ] --- Default tab completer -comp :: Monad m => WordCompleter m -comp n = do - let cmds = [":load", ":type", ":browse", ":quit"] - -- Env.TypeEnv ctx <- gets tyctx - -- let defs = map Data.Text.unpack $ Map.keys ctx - return $ filter (Data.List.isPrefixOf n) (cmds - -- ++ defs - ) +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 @@ -468,8 +546,3 @@ options :: (MonadNix e t f m, MonadIO m) => System.Console.Repline.Options (Repl e t f m) options = (\h -> (helpOptionName h, helpOptionFunction h)) <$> helpOptions - -completer - :: (MonadNix e t f m, MonadIO m) - => CompleterStyle (StateT (IState t f m) m) -completer = Prefix (System.Console.Repline.wordCompleter comp) defaultMatcher From 0cf6be6be206b94d338579ee362e138080c5478c Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Fri, 3 Jul 2020 17:00:03 +0200 Subject: [PATCH 10/11] readme: add short REPL section --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index b3ca937..4a9b499 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,23 @@ $ env NIXPKGS_TESTS=yes PRETTY_TESTS=1 cabal v2-test $ ./dist/build/hnix/hnix --help ``` +## Using the REPL + +To enter the `hnix` REPL use + +``` +hnix --repl +``` + +To evaluate an expression and make it available in the REPL +as the `input` variable use + +``` +hnix --eval -E '(import {}).pkgs.hello' --repl +``` + +Use the `:help` command for a list of all available REPL commands. + ## Building with full debug info To build `hnix` for debugging, and with full tracing output and stack traces, From c9eb24626a6d3a15baed7e6af90f7fb8065fbfef Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Fri, 3 Jul 2020 17:02:51 +0200 Subject: [PATCH 11/11] repl: drop GHC pragams disabling warnings, prune imports --- main/Repl.hs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/main/Repl.hs b/main/Repl.hs index 2958f7d..4834fcf 100644 --- a/main/Repl.hs +++ b/main/Repl.hs @@ -18,9 +18,6 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeSynonymInstances #-} -{-# OPTIONS_GHC -Wno-unused-matches #-} -{-# OPTIONS_GHC -Wno-unused-imports #-} - module Repl ( main , main' @@ -29,16 +26,10 @@ module Repl import Nix hiding ( exec , try ) -import Nix.Cited -import Nix.Convert -import Nix.Eval import Nix.Scope -import qualified Nix.Type.Env as Env -import Nix.Type.Infer import Nix.Utils import Nix.Value.Monad (demand) -import Control.Comonad import qualified Data.List import qualified Data.Maybe import qualified Data.HashMap.Lazy @@ -67,7 +58,6 @@ import System.Console.Repline ( Cmd , CompleterStyle (Prefix) , ExitDecision(Exit) , HaskelineT - , WordCompleter ) import qualified System.Console.Repline import qualified System.Exit @@ -208,8 +198,11 @@ exec update source = do -- Type Inference ( returns Typing Environment ) -- - --let tyctx' = inferTop Env.empty [("repl", stripAnnotation expr)] - --liftIO $ print tyctx' + -- 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)