libexpr: show all missing and unexpected arguments instead of just one

also only give suggestions for unused arguments

Change-Id: Iae8e72defbbe2571a803dfd7216745b39848ccb2
This commit is contained in:
Zitrone
2025-03-11 20:21:24 +01:00
parent 8f871210b5
commit f6c4034e2f
12 changed files with 158 additions and 34 deletions
+4
View File
@@ -149,6 +149,10 @@ puck:
forgejo: puck
github: puckipedia
quantenzitrone:
display_name: Zitrone
forgejo: quantenzitrone
quantumjump:
display_name: Quantum Jump
github: QuantumBJump
@@ -0,0 +1,29 @@
---
synopsis: 'Show all missing and unexpected arguments in errorneous function calls'
issues: []
cls: [2477]
category: Improvements
credits: [quantenzitrone]
---
When calling a function that expects an attribute set, lix will now show all
missing and unexpected arguments.
e.g. with `({ a, b, c } : a + b + c) { a = 1; d = 1; }` lix will now show the error:
```
[...]
error: function 'anonymous lambda' called without required arguments 'b' and 'c' and with unexpected argument 'd'
[...]
```
Previously lix would just show `b`.
Furthermore lix will now only suggest arguments that aren't yet used.
e.g. with `({ a?1, b?1, c?1 } : a + b + c) { a = 1; d = 1; e = 1; }` lix will now show the error:
```
[...]
error: function 'anonymous lambda' called with unexpected arguments 'd' and 'e'
at «string»:1:2:
1| ({ a?1, b?1, c?1 } : a + b + c) { a = 1; d = 1; e = 1; }
| ^
Did you mean one of b or c?
```
Previously lix would also suggest `a`.
Suggestions are unfortunately still currently just for the first missing argument.
+52 -33
View File
@@ -1,8 +1,11 @@
#include "lix/libexpr/eval.hh"
#include "lix/libexpr/eval-settings.hh"
#include "lix/libutil/archive.hh"
#include "lix/libutil/ansicolor.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/english.hh"
#include "lix/libutil/fmt.hh"
#include "lix/libutil/hash.hh"
#include "lix/libexpr/primops.hh"
#include "lix/libexpr/print-options.hh"
@@ -23,12 +26,15 @@
#include "lix/libfetchers/fetch-to-store.hh"
#include "lix/libexpr/flake/flakeref.hh"
#include "lix/libutil/exit.hh"
#include "symbol-table.hh"
#include <algorithm>
#include <iostream>
#include <ostream>
#include <sstream>
#include <cstring>
#include <optional>
#include <string>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
@@ -1428,23 +1434,22 @@ public:
};
};
/** Currently these each just take one, but maybe in the future we could have diagnostics
* for all unexpected and missing arguments?
*/
struct FormalsMatch
{
std::vector<Symbol> missing;
std::vector<Symbol> unexpected;
std::vector<SymbolStr> missing;
std::vector<SymbolStr> unexpected;
std::set<std::string> unused;
};
/** Matchup an attribute argument set to a lambda's formal arguments,
* or return what arguments were required but not given, or given but not allowed.
* (currently returns only one, for each).
*/
FormalsMatch matchupLambdaAttrs(EvalState & state, Env & env, Displacement & displ, AttrsPattern const & pattern, Bindings & attrs)
FormalsMatch matchupLambdaAttrs(EvalState & state, Env & env, Displacement & displ, AttrsPattern const & pattern, Bindings & attrs, SymbolTable & symbols)
{
size_t attrsUsed = 0;
FormalsMatch result;
for (auto const & formal : pattern.formals) {
// The attribute whose name matches the name of the formal we're matching up, if it exists.
@@ -1459,15 +1464,14 @@ FormalsMatch matchupLambdaAttrs(EvalState & state, Env & env, Displacement & dis
}
// The argument for this formal wasn't given.
result.unused.insert(symbols[formal.name]);
// If the formal has a default, use it.
if (formal.def) {
env.values[displ] = formal.def->maybeThunk(state, env);
displ += 1;
} else {
// Otherwise, let our caller know what was missing.
return FormalsMatch{
.missing = {formal.name},
};
result.missing.push_back(symbols[formal.name]);
}
}
@@ -1476,16 +1480,12 @@ FormalsMatch matchupLambdaAttrs(EvalState & state, Env & env, Displacement & dis
// Return the first unexpected argument.
for (Attr const & attr : attrs) {
if (!pattern.has(attr.name)) {
return FormalsMatch{
.unexpected = {attr.name},
};
result.unexpected.push_back(symbols[attr.name]);
}
}
abort(); // unreachable.
}
return FormalsMatch{};
return result;
}
Env & SimplePattern::match(ExprLambda & lambda, EvalState & state, Env & up, Value * arg, const PosIdx pos)
@@ -1522,25 +1522,44 @@ Env & AttrsPattern::match(ExprLambda & lambda, EvalState & state, Env & up, Valu
env2,
displ,
*this,
*arg->attrs
*arg->attrs,
ctx.symbols
);
for (auto const & missingArg : formalsMatch.missing) {
auto const missing = ctx.symbols[missingArg];
ctx.errors.make<TypeError>("function '%s' called without required argument '%s'", lambda.getName(ctx.symbols), missing)
.atPos(lambda.pos)
.withTrace(pos, "from call site")
.withFrame(up, lambda)
.debugThrow();
}
for (auto const & unexpectedArg : formalsMatch.unexpected) {
auto const unex = ctx.symbols[unexpectedArg];
std::set<std::string> formalNames;
for (auto const & formal : formals) {
formalNames.insert(ctx.symbols[formal.name]);
if (!formalsMatch.unexpected.empty() || !formalsMatch.missing.empty()) {
Suggestions sug; // empty suggestions -> no suggestions
if (!formalsMatch.unexpected.empty()) {
// suggestions only for the first unexpected argument
// TODO: suggestions for all unexpected arguments
sug = Suggestions::bestMatches(formalsMatch.unused, formalsMatch.unexpected.front());
}
auto sug = Suggestions::bestMatches(formalNames, unex);
ctx.errors.make<TypeError>("function '%s' called with unexpected argument '%s'", lambda.getName(ctx.symbols), unex)
.atPos(lambda.pos)
auto argFmt = [](const SymbolStr & argument) { return HintFmt("'%s'", argument); };
[&](){
if (formalsMatch.unexpected.empty() && !formalsMatch.missing.empty()) {
return ctx.errors.make<TypeError>(
"function '%s' called without required argument%s %s", lambda.getName(ctx.symbols),
Uncolored((formalsMatch.missing.size() == 1) ? "" : "s"),
Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.missing))
);
} else if (!formalsMatch.unexpected.empty() && formalsMatch.missing.empty()) {
return ctx.errors.make<TypeError>(
"function '%s' called with unexpected argument%s %s", lambda.getName(ctx.symbols),
Uncolored((formalsMatch.unexpected.size() == 1) ? "" : "s"),
Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.unexpected))
);
} else {
return ctx.errors.make<TypeError>(
"function '%s' called without required argument%s %s and with unexpected argument%s %s", lambda.getName(ctx.symbols),
Uncolored((formalsMatch.missing.size() == 1) ? "" : "s"),
Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.missing)),
Uncolored((formalsMatch.unexpected.size() == 1) ? "" : "s"),
Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.unexpected))
);
}
}().atPos(lambda.pos)
.withTrace(pos, "from call site")
.withSuggestions(sug)
.withFrame(up, lambda)
+28
View File
@@ -2,6 +2,10 @@
///@file
#include <iostream>
#include <algorithm>
#include <iterator>
#include <ranges>
#include <sstream>
namespace nix {
@@ -16,4 +20,28 @@ std::ostream & pluralize(
const std::string_view single,
const std::string_view plural);
/** Concatenates a given iterator of strings with commas and 'and',
* while transforming them with the given function.
* e.g. ["foo", "bar", "baz"] might get concatenated to "foobar, barbar and bazbar".
* the lambda for this might look like this:
* `[](std::string & arg) { return arg + "bar"; };`
*/
template<typename F, std::ranges::input_range R>
std::string concatStringsCommaAnd(F transform, const R & args)
{
std::stringstream result;
if (args.size() != 0) {
result << transform(*args.begin());
}
if (args.size() >= 2) {
// will not do anything for size <= 2
std::for_each(std::next(args.begin()), std::prev(args.end()), [&](auto const & arg) {
result << ", " << transform(arg);
});
result << " and " << transform(*args.rbegin());
}
return result.str();
};
}
@@ -0,0 +1,13 @@
error:
… from call site
at /pwd/lang/eval-fail-missing-and-unexpected-args.nix:1:1:
1| ({a, b, c}: a + b + c) {c = 1; d = 1; e = 1; f = 1;}
| ^
2|
error: function 'anonymous lambda' called without required arguments 'a' and 'b' and with unexpected arguments 'd', 'e' and 'f'
at /pwd/lang/eval-fail-missing-and-unexpected-args.nix:1:2:
1| ({a, b, c}: a + b + c) {c = 1; d = 1; e = 1; f = 1;}
| ^
2|
Did you mean one of a or b?
@@ -0,0 +1 @@
({a, b, c}: a + b + c) {c = 1; d = 1; e = 1; f = 1;}
@@ -0,0 +1,12 @@
error:
… from call site
at /pwd/lang/eval-fail-missing-args.nix:1:1:
1| ({a, b, c, d}: a + b + c + d) { }
| ^
2|
error: function 'anonymous lambda' called without required arguments 'a', 'b', 'c' and 'd'
at /pwd/lang/eval-fail-missing-args.nix:1:2:
1| ({a, b, c, d}: a + b + c + d) { }
| ^
2|
@@ -0,0 +1 @@
({a, b, c, d}: a + b + c + d) { }
@@ -10,4 +10,3 @@ error:
1| ({x, z}: x + z) {x = "foo"; y = "bla"; z = "bar";}
| ^
2|
Did you mean one of x or z?
@@ -0,0 +1,13 @@
error:
… from call site
at /pwd/lang/eval-fail-undeclared-args.nix:1:1:
1| ({a ? "foo", b ? "bar"}: a + b) {c = "meow"; d = "meow";}
| ^
2|
error: function 'anonymous lambda' called with unexpected arguments 'c' and 'd'
at /pwd/lang/eval-fail-undeclared-args.nix:1:2:
1| ({a ? "foo", b ? "bar"}: a + b) {c = "meow"; d = "meow";}
| ^
2|
Did you mean one of a or b?
@@ -0,0 +1 @@
({a ? "foo", b ? "bar"}: a + b) {c = "meow"; d = "meow";}
+4
View File
@@ -40,5 +40,9 @@ NIX_EVAL_STDERR_WITH_SUGGESTIONS=$(! nix build --impure --expr '(builtins.getFla
fail "The evaluator should suggest the three closest possiblities"
NIX_EVAL_STDERR_WITH_SUGGESTIONS=$(! nix build --impure --expr '({ foo }: foo) { foo = 1; fob = 2; }' 2>&1 1>/dev/null)
[[ ! "$NIX_EVAL_STDERR_WITH_SUGGESTIONS" =~ "Did you mean" ]] || \
fail "The evaluator shouldn't suggest anything if all arguments are already provided."
NIX_EVAL_STDERR_WITH_SUGGESTIONS=$(! nix build --impure --expr '({ foo ? 1 }: foo) { fob = 2; }' 2>&1 1>/dev/null)
[[ "$NIX_EVAL_STDERR_WITH_SUGGESTIONS" =~ "Did you mean foo?" ]] || \
fail "The evaluator should suggest the three closest possiblities"