diff --git a/doc/manual/rl-next/repl-inherit.md b/doc/manual/rl-next/repl-inherit.md new file mode 100644 index 00000000000..295d70323e4 --- /dev/null +++ b/doc/manual/rl-next/repl-inherit.md @@ -0,0 +1,21 @@ +--- +synopsis: "`nix repl` now supports `inherit` and multiple bindings" +prs: [15082] +--- + +The `nix repl` now supports `inherit` statements and multiple bindings per line: + +``` +nix-repl> a = { x = 1; y = 2; } +nix-repl> inherit (a) x y +nix-repl> x + y +3 + +nix-repl> p = 1; q = 2; +nix-repl> p + q +3 + +nix-repl> foo.bar.baz = 1; +nix-repl> foo.bar +{ baz = 1; } +``` diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 47d0127a322..5eeb21aa0b5 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -100,6 +100,7 @@ struct NixRepl : AbstractNixRepl, detail::ReplCompleterMixin, gc void addAttrsToScope(Value & attrs); void addVarToScope(const Symbol name, Value & v); Expr * parseString(std::string s); + ExprAttrs * parseReplBindings(std::string s); void evalString(std::string s, Value & v); void loadDebugTraceEnv(DebugTrace & dt); @@ -309,21 +310,6 @@ StringSet NixRepl::completePrefix(const std::string & prefix) return completions; } -// FIXME: DRY and match or use the parser -static bool isVarName(std::string_view s) -{ - if (s.size() == 0) - return false; - char c = s[0]; - if ((c >= '0' && c <= '9') || c == '-' || c == '\'') - return false; - for (auto & i : s) - if (!((i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z') || (i >= '0' && i <= '9') || i == '_' || i == '-' - || i == '\'')) - return false; - return true; -} - StorePath NixRepl::getDerivationPath(Value & v) { auto packageInfo = getDerivation(*state, v, false); @@ -693,15 +679,22 @@ ProcessLineResult NixRepl::processLine(std::string line) throw Error("unknown command '%1%'", command); else { - size_t p = line.find('='); - std::string name; - if (p != std::string::npos && p < line.size() && line[p + 1] != '=' - && isVarName(name = removeWhitespace(line.substr(0, p)))) { - Expr * e = parseString(line.substr(p + 1)); - Value & v(*state->allocValue()); - v.mkThunk(env, e); - addVarToScope(state->symbols.create(name), v); + // Try parsing as bindings first (handles `x = 1`, `inherit ...`, etc.) + ExprAttrs * bindings = nullptr; + try { + bindings = parseReplBindings(line); + } catch (ParseError &) { + } + + if (bindings) { + Env * inheritEnv = bindings->inheritFromExprs ? bindings->buildInheritFromEnv(*state, *env) : nullptr; + for (auto & [symbol, def] : *bindings->attrs) { + Value & v(*state->allocValue()); + v.mkThunk(def.chooseByKind(env, env, inheritEnv), def.e); + addVarToScope(symbol, v); + } } else { + // Otherwise evaluate as expression Value v; evalString(line, v); auto suspension = logger->suspend(); @@ -882,6 +875,28 @@ Expr * NixRepl::parseString(std::string s) } } +ExprAttrs * NixRepl::parseReplBindings(std::string s) +{ + auto basePath = state->rootPath("."); + + // Try parsing as bindings + std::exception_ptr bindingsError; + try { + return state->parseReplBindings(s, basePath, staticEnv); + } catch (ParseError &) { + bindingsError = std::current_exception(); + } + + // Try with semicolon appended (for `inherit foo` shorthand) + // Use original source (s) for error messages, not s + ";" + try { + return state->parseReplBindings(s + ";", s, basePath, staticEnv); + } catch (ParseError &) { + // Semicolon retry failed; rethrow the original bindings error + std::rethrow_exception(bindingsError); + } +} + void NixRepl::evalString(std::string s, Value & v) { Expr * e = parseString(s); diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 84f14198f82..698558abdca 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -3139,6 +3139,21 @@ Expr * EvalState::parseExprFromString(std::string s, const SourcePath & basePath return parseExprFromString(std::move(s), basePath, staticBaseEnv); } +ExprAttrs * +EvalState::parseReplBindings(std::string s_, const SourcePath & basePath, const std::shared_ptr & staticEnv) +{ + return parseReplBindings(s_, s_, basePath, staticEnv); +} + +ExprAttrs * EvalState::parseReplBindings( + std::string s_, std::string errorSource, const SourcePath & basePath, const std::shared_ptr & staticEnv) +{ + auto s = make_ref(std::move(errorSource)); + // flex requires two NUL terminators for yy_scan_buffer + s_.append("\0\0", 2); + return parseReplBindings(s_.data(), s_.size(), Pos::String{.source = s}, basePath, staticEnv); +} + Expr * EvalState::parseStdin() { // NOTE this method (and parseExprFromString) must take care to *fully copy* their @@ -3280,6 +3295,30 @@ Expr * EvalState::parse( return result; } +ExprAttrs * EvalState::parseReplBindings( + char * text, + size_t length, + Pos::Origin origin, + const SourcePath & basePath, + const std::shared_ptr & staticEnv) +{ + DocCommentMap tmpDocComments; + DocCommentMap * docComments = &tmpDocComments; + + if (auto sourcePath = std::get_if(&origin)) { + auto [it, _] = positionToDocComment.try_emplace(*sourcePath); + docComments = &it->second; + } + + auto bindings = parseReplBindingsFromBuf( + text, length, origin, basePath, mem.exprs, symbols, settings, positions, *docComments, rootFS); + assert(bindings); + + bindings->bindVars(*this, staticEnv); + + return bindings; +} + DocComment EvalState::getDocCommentForPos(PosIdx pos) { auto pos2 = positions[pos]; diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh index 29acb4547aa..22f6854cb65 100644 --- a/src/libexpr/include/nix/expr/eval.hh +++ b/src/libexpr/include/nix/expr/eval.hh @@ -603,6 +603,18 @@ public: parseExprFromString(std::string s, const SourcePath & basePath, const std::shared_ptr & staticEnv); Expr * parseExprFromString(std::string s, const SourcePath & basePath); + /** + * Parse REPL bindings from the specified string. + * Returns ExprAttrs with bindings to add to scope. + */ + ExprAttrs * + parseReplBindings(std::string s, const SourcePath & basePath, const std::shared_ptr & staticEnv); + ExprAttrs * parseReplBindings( + std::string s, + std::string errorSource, + const SourcePath & basePath, + const std::shared_ptr & staticEnv); + Expr * parseStdin(); /** @@ -867,6 +879,13 @@ private: const SourcePath & basePath, const std::shared_ptr & staticEnv); + ExprAttrs * parseReplBindings( + char * text, + size_t length, + Pos::Origin origin, + const SourcePath & basePath, + const std::shared_ptr & staticEnv); + /** * Current Nix call stack depth, used with `max-call-depth` setting to throw stack overflow hopefully before we run * out of system stack. diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l index 810503bdc5b..5bdb5335b84 100644 --- a/src/libexpr/lexer.l +++ b/src/libexpr/lexer.l @@ -14,6 +14,7 @@ %x INPATH %x INPATH_SLASH %x PATH_START +%x REPL_BINDINGS_MODE %top { #include "parser-tab.hh" // YYSTYPE @@ -113,6 +114,13 @@ URI [a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~ %% + /* REPL bindings mode: inject REPL_BINDINGS token at start, then switch to normal lexing */ +.|\n { + yyless(0); + yylloc->unstash(); + POP_STATE(); + return REPL_BINDINGS; +} if { return IF; } then { return THEN; } @@ -339,3 +347,17 @@ or { return OR_KW; } } %% + +#include + +// Verify that the forward declaration in parser.y matches flex's definition +static_assert(std::is_same_v); + +namespace nix { + +void setReplBindingsMode(yyscan_t scanner) +{ + yy_push_state(REPL_BINDINGS_MODE, scanner); +} + +} diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 7d78fb84137..3bee5e8ab26 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -54,6 +54,11 @@ } \ while (0) +// Forward declaration for flex scanner type. +// lexer-tab.hh is large; avoid including it just for this type. +// Asserted with static_assert in lexer.l. +typedef void * yyscan_t; + namespace nix { typedef boost::unordered_flat_map> DocCommentMap; @@ -70,6 +75,28 @@ Expr * parseExprFromBuf( DocCommentMap & docComments, const ref rootFS); +/** + * Puts the lexer in REPL bindings mode before the first token. This causes + * the parser to accept REPL bindings (attribute definitions). + */ +void setReplBindingsMode(yyscan_t scanner); + +/** + * Parse REPL bindings from a buffer. + * Returns ExprAttrs with bindings to add to scope. + */ +ExprAttrs * parseReplBindingsFromBuf( + char * text, + size_t length, + Pos::Origin origin, + const SourcePath & basePath, + Exprs & exprs, + SymbolTable & symbols, + const EvalSettings & settings, + PosTable & positions, + DocCommentMap & docComments, + const ref rootFS); + } #endif @@ -175,6 +202,7 @@ static Expr * makeCall(Exprs & exprs, PosIdx pos, Expr * fn, Expr * arg) { %token IND_STRING_OPEN "start of an indented string" %token IND_STRING_CLOSE "end of an indented string" %token ELLIPSIS "'...'" +%token REPL_BINDINGS "start of REPL bindings" %right IMPL %left OR @@ -196,6 +224,10 @@ start: expr { // This parser does not use yynerrs; suppress the warning. (void) yynerrs_; +} +| REPL_BINDINGS binds1 { + state->result = $2; + (void) yynerrs_; }; expr: expr_function; @@ -577,6 +609,51 @@ Expr * parseExprFromBuf( return state.result; } +ExprAttrs * parseReplBindingsFromBuf( + char * text, + size_t length, + Pos::Origin origin, + const SourcePath & basePath, + Exprs & exprs, + SymbolTable & symbols, + const EvalSettings & settings, + PosTable & positions, + DocCommentMap & docComments, + const ref rootFS) +{ + yyscan_t scanner; + LexerState lexerState { + .positionToDocComment = docComments, + .positions = positions, + .origin = positions.addOrigin(origin, length), + }; + ParserState state { + .lexerState = lexerState, + .exprs = exprs, + .symbols = symbols, + .positions = positions, + .basePath = basePath, + .origin = lexerState.origin, + .rootFS = rootFS, + .settings = settings, + }; + + yylex_init_extra(&lexerState, &scanner); + Finally _destroy([&] { yylex_destroy(scanner); }); + + yy_scan_buffer(text, length, scanner); + setReplBindingsMode(scanner); + Parser parser(scanner, &state); + parser.parse(); + + assert(state.result); + // state.result is Expr *, but the REPL_BINDINGS grammar rule + // always produces an ExprAttrs via the binds1 production. + auto bindings = dynamic_cast(state.result); + assert(bindings); + return bindings; +} + } #pragma GCC diagnostic pop // end ignored "-Wswitch-enum" diff --git a/tests/functional/repl.sh b/tests/functional/repl.sh index 7023f2b8a0d..3959e694489 100755 --- a/tests/functional/repl.sh +++ b/tests/functional/repl.sh @@ -139,6 +139,64 @@ testReplResponseNoRegex ' "$" + "{hi}" ' '"\${hi}"' +# Test inherit statement support (issue #15053) +testReplResponseNoRegex ' +a = { b = 1; c = 2; } +inherit (a) b +b +' '1' + +# inherit multiple attributes +testReplResponseNoRegex ' +a = { x = 10; y = 20; } +inherit (a) x y +x + y +' '30' + +# inherit from current scope +testReplResponseNoRegex ' +foo = 42 +inherit foo +foo +' '42' + +# inherit with semicolon (also works) +testReplResponseNoRegex ' +a = { z = 99; } +inherit (a) z; +z +' '99' + +# multiple bindings on one line +testReplResponseNoRegex ' +a = 1; b = 2; +a + b +' '3' + +# nested attribute path +testReplResponseNoRegex ' +a.b.c = 1; +a.b +' '{ c = 1; }' + +# mixed bindings: inherit and assignment +testReplResponseNoRegex ' +x = { p = 10; } +inherit (x) p; q = 20; +p + q +' '30' + +# inherit error shows position (without spurious semicolon from retry) +testReplResponse ' +a = { x = 1; } +inherit (a) y +y +' "error: attribute 'y' missing +.*at .string.:1:13: +.*inherit (a) y +.* \\^ +.*Did you mean x" + testReplResponse ' drvPath ' '".*-simple.drv"' \