mirror of
https://github.com/NixOS/nix.git
synced 2025-11-08 19:46:02 +01:00
libexpr: improve error messages for builtins.genericClosure
Show which element(s) are involved at each error point: - When an element is missing the "key" attribute, show the element - When an element is not an attribute set, show the element - When comparing keys fails, show both elements being compared - When calling operator fails, show which element was being processed This provides concrete context using ValuePrinter with errorPrintOptions. Note: errorPrintOptions uses maxDepth=10 by default, which may print quite deeply nested structures in error messages. This could potentially be overwhelming, but follows the existing default for error contexts.
This commit is contained in:
parent
ca787bc3e0
commit
d262efc240
10 changed files with 188 additions and 46 deletions
|
|
@ -165,35 +165,48 @@ TEST_F(ErrorTraceTest, genericClosure)
|
||||||
HintFmt("expected a function but found %s: %s", "a Boolean", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)),
|
HintFmt("expected a function but found %s: %s", "a Boolean", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)),
|
||||||
HintFmt("while evaluating the 'operator' attribute passed as argument to builtins.genericClosure"));
|
HintFmt("while evaluating the 'operator' attribute passed as argument to builtins.genericClosure"));
|
||||||
|
|
||||||
ASSERT_TRACE2(
|
ASSERT_TRACE3(
|
||||||
"genericClosure { startSet = [{ key = 1;}]; operator = item: true; }",
|
"genericClosure { startSet = [{ key = 1;}]; operator = item: true; }",
|
||||||
TypeError,
|
TypeError,
|
||||||
HintFmt("expected a list but found %s: %s", "a Boolean", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)),
|
HintFmt("expected a list but found %s: %s", "a Boolean", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)),
|
||||||
HintFmt("while evaluating the return value of the `operator` passed to builtins.genericClosure"));
|
HintFmt("while evaluating the return value of the `operator` passed to builtins.genericClosure"),
|
||||||
|
HintFmt(
|
||||||
|
"while calling %s on genericClosure element %s",
|
||||||
|
"operator",
|
||||||
|
Uncolored("{ key = " ANSI_CYAN "1" ANSI_NORMAL "; }")));
|
||||||
|
|
||||||
ASSERT_TRACE2(
|
ASSERT_TRACE3(
|
||||||
"genericClosure { startSet = [{ key = 1;}]; operator = item: [ true ]; }",
|
"genericClosure { startSet = [{ key = 1;}]; operator = item: [ true ]; }",
|
||||||
TypeError,
|
TypeError,
|
||||||
HintFmt("expected a set but found %s: %s", "a Boolean", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)),
|
HintFmt("expected a set but found %s: %s", "a Boolean", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)),
|
||||||
HintFmt("while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure"));
|
HintFmt(""),
|
||||||
|
HintFmt("in genericClosure element %s", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)));
|
||||||
|
|
||||||
ASSERT_TRACE2(
|
ASSERT_TRACE3(
|
||||||
"genericClosure { startSet = [{ key = 1;}]; operator = item: [ {} ]; }",
|
"genericClosure { startSet = [{ key = 1;}]; operator = item: [ {} ]; }",
|
||||||
TypeError,
|
TypeError,
|
||||||
HintFmt("attribute '%s' missing", "key"),
|
HintFmt("attribute '%s' missing", "key"),
|
||||||
HintFmt("in one of the attrsets generated by (or initially passed to) builtins.genericClosure"));
|
HintFmt(""),
|
||||||
|
HintFmt("in genericClosure element %s", Uncolored("{ }")));
|
||||||
|
|
||||||
ASSERT_TRACE2(
|
ASSERT_TRACE3(
|
||||||
"genericClosure { startSet = [{ key = 1;}]; operator = item: [{ key = ''a''; }]; }",
|
"genericClosure { startSet = [{ key = 1;}]; operator = item: [{ key = ''a''; }]; }",
|
||||||
EvalError,
|
EvalError,
|
||||||
HintFmt("cannot compare %s with %s", "a string", "an integer"),
|
HintFmt(
|
||||||
HintFmt("while comparing the `key` attributes of two genericClosure elements"));
|
"cannot compare %s with %s; values are %s and %s",
|
||||||
|
"a string",
|
||||||
|
"an integer",
|
||||||
|
Uncolored(ANSI_MAGENTA "\"a\"" ANSI_NORMAL),
|
||||||
|
Uncolored(ANSI_CYAN "1" ANSI_NORMAL)),
|
||||||
|
HintFmt("with element %s", Uncolored("{ key = " ANSI_CYAN "1" ANSI_NORMAL "; }")),
|
||||||
|
HintFmt("while comparing element %s", Uncolored("{ key = " ANSI_MAGENTA "\"a\"" ANSI_NORMAL "; }")));
|
||||||
|
|
||||||
ASSERT_TRACE2(
|
ASSERT_TRACE3(
|
||||||
"genericClosure { startSet = [ true ]; operator = item: [{ key = ''a''; }]; }",
|
"genericClosure { startSet = [ true ]; operator = item: [{ key = ''a''; }]; }",
|
||||||
TypeError,
|
TypeError,
|
||||||
HintFmt("expected a set but found %s: %s", "a Boolean", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)),
|
HintFmt("expected a set but found %s: %s", "a Boolean", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)),
|
||||||
HintFmt("while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure"));
|
HintFmt(""),
|
||||||
|
HintFmt("in genericClosure element %s", Uncolored(ANSI_CYAN "true" ANSI_NORMAL)));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(ErrorTraceTest, replaceStrings)
|
TEST_F(ErrorTraceTest, replaceStrings)
|
||||||
|
|
@ -1050,17 +1063,35 @@ TEST_F(ErrorTraceTest, bitXor)
|
||||||
|
|
||||||
TEST_F(ErrorTraceTest, lessThan)
|
TEST_F(ErrorTraceTest, lessThan)
|
||||||
{
|
{
|
||||||
ASSERT_TRACE1("lessThan 1 \"foo\"", EvalError, HintFmt("cannot compare %s with %s", "an integer", "a string"));
|
ASSERT_TRACE1(
|
||||||
|
"lessThan 1 \"foo\"",
|
||||||
|
EvalError,
|
||||||
|
HintFmt(
|
||||||
|
"cannot compare %s with %s; values are %s and %s",
|
||||||
|
"an integer",
|
||||||
|
"a string",
|
||||||
|
Uncolored(ANSI_CYAN "1" ANSI_NORMAL),
|
||||||
|
Uncolored(ANSI_MAGENTA "\"foo\"" ANSI_NORMAL)));
|
||||||
|
|
||||||
ASSERT_TRACE1(
|
ASSERT_TRACE1(
|
||||||
"lessThan {} {}",
|
"lessThan {} {}",
|
||||||
EvalError,
|
EvalError,
|
||||||
HintFmt("cannot compare %s with %s; values of that type are incomparable", "a set", "a set"));
|
HintFmt(
|
||||||
|
"cannot compare %s with %s; values of that type are incomparable (values are %s and %s)",
|
||||||
|
"a set",
|
||||||
|
"a set",
|
||||||
|
Uncolored("{ }"),
|
||||||
|
Uncolored("{ }")));
|
||||||
|
|
||||||
ASSERT_TRACE2(
|
ASSERT_TRACE2(
|
||||||
"lessThan [ 1 2 ] [ \"foo\" ]",
|
"lessThan [ 1 2 ] [ \"foo\" ]",
|
||||||
EvalError,
|
EvalError,
|
||||||
HintFmt("cannot compare %s with %s", "an integer", "a string"),
|
HintFmt(
|
||||||
|
"cannot compare %s with %s; values are %s and %s",
|
||||||
|
"an integer",
|
||||||
|
"a string",
|
||||||
|
Uncolored(ANSI_CYAN "1" ANSI_NORMAL),
|
||||||
|
Uncolored(ANSI_MAGENTA "\"foo\"" ANSI_NORMAL)),
|
||||||
HintFmt("while comparing two list elements"));
|
HintFmt("while comparing two list elements"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,14 @@ struct CompareValues
|
||||||
if (v1->type() == nInt && v2->type() == nFloat)
|
if (v1->type() == nInt && v2->type() == nFloat)
|
||||||
return v1->integer().value < v2->fpoint();
|
return v1->integer().value < v2->fpoint();
|
||||||
if (v1->type() != v2->type())
|
if (v1->type() != v2->type())
|
||||||
state.error<EvalError>("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow();
|
state
|
||||||
|
.error<EvalError>(
|
||||||
|
"cannot compare %s with %s; values are %s and %s",
|
||||||
|
showType(*v1),
|
||||||
|
showType(*v2),
|
||||||
|
ValuePrinter(state, *v1, errorPrintOptions),
|
||||||
|
ValuePrinter(state, *v2, errorPrintOptions))
|
||||||
|
.debugThrow();
|
||||||
// Allow selecting a subset of enum values
|
// Allow selecting a subset of enum values
|
||||||
#pragma GCC diagnostic push
|
#pragma GCC diagnostic push
|
||||||
#pragma GCC diagnostic ignored "-Wswitch-enum"
|
#pragma GCC diagnostic ignored "-Wswitch-enum"
|
||||||
|
|
@ -711,7 +718,11 @@ struct CompareValues
|
||||||
default:
|
default:
|
||||||
state
|
state
|
||||||
.error<EvalError>(
|
.error<EvalError>(
|
||||||
"cannot compare %s with %s; values of that type are incomparable", showType(*v1), showType(*v2))
|
"cannot compare %s with %s; values of that type are incomparable (values are %s and %s)",
|
||||||
|
showType(*v1),
|
||||||
|
showType(*v2),
|
||||||
|
ValuePrinter(state, *v1, errorPrintOptions),
|
||||||
|
ValuePrinter(state, *v2, errorPrintOptions))
|
||||||
.debugThrow();
|
.debugThrow();
|
||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
}
|
}
|
||||||
|
|
@ -757,42 +768,79 @@ static void prim_genericClosure(EvalState & state, const PosIdx pos, Value ** ar
|
||||||
`workSet', adding the result to `workSet', continuing until
|
`workSet', adding the result to `workSet', continuing until
|
||||||
no new elements are found. */
|
no new elements are found. */
|
||||||
ValueList res;
|
ValueList res;
|
||||||
// `doneKeys' doesn't need to be a GC root, because its values are
|
// Track which element each key came from
|
||||||
// reachable from res.
|
auto cmp = CompareValues(state, noPos, "");
|
||||||
auto cmp = CompareValues(state, noPos, "while comparing the `key` attributes of two genericClosure elements");
|
std::map<Value *, Value *, decltype(cmp)> keyToElem(cmp);
|
||||||
std::set<Value *, decltype(cmp)> doneKeys(cmp);
|
|
||||||
while (!workSet.empty()) {
|
while (!workSet.empty()) {
|
||||||
Value * e = *(workSet.begin());
|
Value * e = *(workSet.begin());
|
||||||
workSet.pop_front();
|
workSet.pop_front();
|
||||||
|
|
||||||
state.forceAttrs(
|
try {
|
||||||
*e,
|
state.forceAttrs(*e, noPos, "");
|
||||||
noPos,
|
} catch (Error & err) {
|
||||||
"while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure");
|
err.addTrace(nullptr, "in genericClosure element %s", ValuePrinter(state, *e, errorPrintOptions));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
auto key = state.getAttr(
|
const Attr * key;
|
||||||
state.s.key,
|
try {
|
||||||
e->attrs(),
|
key = state.getAttr(state.s.key, e->attrs(), "");
|
||||||
"in one of the attrsets generated by (or initially passed to) builtins.genericClosure");
|
} catch (Error & err) {
|
||||||
|
err.addTrace(nullptr, "in genericClosure element %s", ValuePrinter(state, *e, errorPrintOptions));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
state.forceValue(*key->value, noPos);
|
state.forceValue(*key->value, noPos);
|
||||||
|
|
||||||
if (!doneKeys.insert(key->value).second)
|
try {
|
||||||
continue;
|
auto [it, inserted] = keyToElem.insert({key->value, e});
|
||||||
|
if (!inserted)
|
||||||
|
continue;
|
||||||
|
} catch (Error & err) {
|
||||||
|
// Try to find which element we're comparing against
|
||||||
|
Value * otherElem = nullptr;
|
||||||
|
for (auto & [otherKey, elem] : keyToElem) {
|
||||||
|
try {
|
||||||
|
cmp(key->value, otherKey);
|
||||||
|
} catch (Error &) {
|
||||||
|
// Found the element we're comparing against
|
||||||
|
otherElem = elem;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (otherElem) {
|
||||||
|
// Traces are printed in reverse order; pre-swap them.
|
||||||
|
err.addTrace(nullptr, "with element %s", ValuePrinter(state, *otherElem, errorPrintOptions));
|
||||||
|
err.addTrace(nullptr, "while comparing element %s", ValuePrinter(state, *e, errorPrintOptions));
|
||||||
|
} else {
|
||||||
|
// Couldn't find the specific element, just show current
|
||||||
|
err.addTrace(nullptr, "while checking key of element %s", ValuePrinter(state, *e, errorPrintOptions));
|
||||||
|
}
|
||||||
|
throw;
|
||||||
|
}
|
||||||
res.push_back(e);
|
res.push_back(e);
|
||||||
|
|
||||||
/* Call the `operator' function with `e' as argument. */
|
/* Call the `operator' function with `e' as argument. */
|
||||||
Value newElements;
|
Value newElements;
|
||||||
state.callFunction(*op->value, {&e, 1}, newElements, noPos);
|
try {
|
||||||
state.forceList(
|
state.callFunction(*op->value, {&e, 1}, newElements, noPos);
|
||||||
newElements,
|
state.forceList(
|
||||||
noPos,
|
newElements,
|
||||||
"while evaluating the return value of the `operator` passed to builtins.genericClosure");
|
noPos,
|
||||||
|
"while evaluating the return value of the `operator` passed to builtins.genericClosure");
|
||||||
|
|
||||||
/* Add the values returned by the operator to the work set. */
|
/* Add the values returned by the operator to the work set. */
|
||||||
for (auto elem : newElements.listView()) {
|
for (auto elem : newElements.listView()) {
|
||||||
state.forceValue(*elem, noPos); // "while evaluating one one of the elements returned by the `operator`
|
state.forceValue(*elem, noPos); // "while evaluating one one of the elements returned by the `operator`
|
||||||
// passed to builtins.genericClosure");
|
// passed to builtins.genericClosure");
|
||||||
workSet.push_back(elem);
|
workSet.push_back(elem);
|
||||||
|
}
|
||||||
|
} catch (Error & err) {
|
||||||
|
err.addTrace(
|
||||||
|
nullptr,
|
||||||
|
"while calling %s on genericClosure element %s",
|
||||||
|
state.symbols[state.s.operator_],
|
||||||
|
ValuePrinter(state, *e, errorPrintOptions));
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
error:
|
||||||
|
… while calling the 'seq' builtin
|
||||||
|
at /pwd/lang/eval-fail-genericClosure-deeply-nested-element.nix:25:1:
|
||||||
|
24| in
|
||||||
|
25| builtins.seq finiteVal (
|
||||||
|
| ^
|
||||||
|
26| builtins.genericClosure {
|
||||||
|
|
||||||
|
… while calling the 'genericClosure' builtin
|
||||||
|
at /pwd/lang/eval-fail-genericClosure-deeply-nested-element.nix:26:3:
|
||||||
|
25| builtins.seq finiteVal (
|
||||||
|
26| builtins.genericClosure {
|
||||||
|
| ^
|
||||||
|
27| startSet = [
|
||||||
|
|
||||||
|
… in genericClosure element { finite = { a0 = { a1 = { a2 = { a3 = { a4 = { a5 = { a6 = { a7 = { a8 = { ... }; }; }; }; }; }; }; }; }; }; «1 attribute elided» }
|
||||||
|
|
||||||
|
error: attribute 'key' missing
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
let
|
||||||
|
finite = {
|
||||||
|
a0 = {
|
||||||
|
a1 = {
|
||||||
|
a2 = {
|
||||||
|
a3 = {
|
||||||
|
a4 = {
|
||||||
|
a5 = {
|
||||||
|
a6 = {
|
||||||
|
a7 = {
|
||||||
|
a8 = {
|
||||||
|
a9 = "deep";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
finiteVal = builtins.deepSeq finite finite;
|
||||||
|
in
|
||||||
|
builtins.seq finiteVal (
|
||||||
|
builtins.genericClosure {
|
||||||
|
startSet = [
|
||||||
|
{
|
||||||
|
infinite = import ./infinite-nesting.nix;
|
||||||
|
finite = finiteVal;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
operator = x: [ (import ./infinite-nesting.nix) ];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -5,6 +5,6 @@ error:
|
||||||
| ^
|
| ^
|
||||||
2| startSet = [ { nokey = 1; } ];
|
2| startSet = [ { nokey = 1; } ];
|
||||||
|
|
||||||
… in one of the attrsets generated by (or initially passed to) builtins.genericClosure
|
… in genericClosure element { nokey = 1; }
|
||||||
|
|
||||||
error: attribute 'key' missing
|
error: attribute 'key' missing
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@ error:
|
||||||
| ^
|
| ^
|
||||||
2| startSet = [ "not an attrset" ];
|
2| startSet = [ "not an attrset" ];
|
||||||
|
|
||||||
… while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure
|
… in genericClosure element "not an attrset"
|
||||||
|
|
||||||
error: expected a set but found a string: "not an attrset"
|
error: expected a set but found a string: "not an attrset"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ error:
|
||||||
| ^
|
| ^
|
||||||
2| startSet = [
|
2| startSet = [
|
||||||
|
|
||||||
… while comparing the `key` attributes of two genericClosure elements
|
… while comparing element { key = "string"; }
|
||||||
|
|
||||||
error: cannot compare a string with an integer
|
… with element { key = 1; }
|
||||||
|
|
||||||
|
error: cannot compare a string with an integer; values are "string" and 1
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ error:
|
||||||
| ^
|
| ^
|
||||||
2| startSet = [
|
2| startSet = [
|
||||||
|
|
||||||
… while comparing the `key` attributes of two genericClosure elements
|
… while comparing element { key = { }; }
|
||||||
|
|
||||||
error: cannot compare a set with a set; values of that type are incomparable
|
… with element { key = { }; }
|
||||||
|
|
||||||
|
error: cannot compare a set with a set; values of that type are incomparable (values are { } and { })
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ error:
|
||||||
| ^
|
| ^
|
||||||
2| startSet = [ { key = 1; } ];
|
2| startSet = [ { key = 1; } ];
|
||||||
|
|
||||||
|
… while calling operator on genericClosure element { key = 1; }
|
||||||
|
|
||||||
… while evaluating the return value of the `operator` passed to builtins.genericClosure
|
… while evaluating the return value of the `operator` passed to builtins.genericClosure
|
||||||
|
|
||||||
error: expected a list but found a string: "not a list"
|
error: expected a list but found a string: "not a list"
|
||||||
|
|
|
||||||
4
tests/functional/lang/infinite-nesting.nix
Normal file
4
tests/functional/lang/infinite-nesting.nix
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
let
|
||||||
|
mkInfinite = i: { "a${toString i}" = mkInfinite (i + 1); };
|
||||||
|
in
|
||||||
|
mkInfinite 0
|
||||||
Loading…
Add table
Add a link
Reference in a new issue