Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/manual/rl-next/repl-inherit.md
Original file line number Diff line number Diff line change
@@ -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; }
```
61 changes: 38 additions & 23 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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> & staticEnv)
{
return parseReplBindings(s_, s_, basePath, staticEnv);
}

ExprAttrs * EvalState::parseReplBindings(
std::string s_, std::string errorSource, const SourcePath & basePath, const std::shared_ptr<StaticEnv> & staticEnv)
{
auto s = make_ref<std::string>(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
Expand Down Expand Up @@ -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> & staticEnv)
{
DocCommentMap tmpDocComments;
DocCommentMap * docComments = &tmpDocComments;

if (auto sourcePath = std::get_if<SourcePath>(&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];
Expand Down
19 changes: 19 additions & 0 deletions src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,18 @@ public:
parseExprFromString(std::string s, const SourcePath & basePath, const std::shared_ptr<StaticEnv> & 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> & staticEnv);
ExprAttrs * parseReplBindings(
std::string s,
std::string errorSource,
const SourcePath & basePath,
const std::shared_ptr<StaticEnv> & staticEnv);

Expr * parseStdin();

/**
Expand Down Expand Up @@ -867,6 +879,13 @@ private:
const SourcePath & basePath,
const std::shared_ptr<StaticEnv> & staticEnv);

ExprAttrs * parseReplBindings(
char * text,
size_t length,
Pos::Origin origin,
const SourcePath & basePath,
const std::shared_ptr<StaticEnv> & staticEnv);

/**
* Current Nix call stack depth, used with `max-call-depth` setting to throw stack overflow hopefully before we run
* out of system stack.
Expand Down
22 changes: 22 additions & 0 deletions src/libexpr/lexer.l
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
%x INPATH
%x INPATH_SLASH
%x PATH_START
%x REPL_BINDINGS_MODE

%top {
#include "parser-tab.hh" // YYSTYPE
Expand Down Expand Up @@ -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 */
<REPL_BINDINGS_MODE>.|\n {
yyless(0);
yylloc->unstash();
POP_STATE();
return REPL_BINDINGS;
}

if { return IF; }
then { return THEN; }
Expand Down Expand Up @@ -339,3 +347,17 @@ or { return OR_KW; }
}

%%

#include <type_traits>

// Verify that the forward declaration in parser.y matches flex's definition
static_assert(std::is_same_v<yyscan_t, void *>);

namespace nix {

void setReplBindingsMode(yyscan_t scanner)
{
yy_push_state(REPL_BINDINGS_MODE, scanner);
}

}
77 changes: 77 additions & 0 deletions src/libexpr/parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -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<PosIdx, DocComment, std::hash<PosIdx>> DocCommentMap;
Expand All @@ -70,6 +75,28 @@ Expr * parseExprFromBuf(
DocCommentMap & docComments,
const ref<SourceAccessor> 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<SourceAccessor> rootFS);

}

#endif
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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<SourceAccessor> 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<ExprAttrs *>(state.result);
assert(bindings);
return bindings;
}


}
#pragma GCC diagnostic pop // end ignored "-Wswitch-enum"
Loading
Loading