aboutsummaryrefslogtreecommitdiff
path: root/src/libexpr/eval.cc
diff options
context:
space:
mode:
authoreldritch horrors <pennae@lix.systems>2024-03-08 07:09:48 +0100
committereldritch horrors <pennae@lix.systems>2024-03-09 04:47:05 -0700
commit08252967a8c2ab15f3fb8bdfb848f007d4032d50 (patch)
tree909ec8357654d4d179f0f8e7dfc5d3d623256b46 /src/libexpr/eval.cc
parentd4c738fe4c587c3f09b0fda899f419f2de97ee2f (diff)
libexpr: Support structured error classes
While preparing PRs like #9753, I've had to change error messages in dozens of code paths. It would be nice if instead of EvalError("expected 'boolean' but found '%1%'", showType(v)) we could write TypeError(v, "boolean") or similar. Then, changing the error message could be a mechanical refactor with the compiler pointing out places the constructor needs to be changed, rather than the error-prone process of grepping through the codebase. Structured errors would also help prevent the "same" error from having multiple slightly different messages, and could be a first step towards error codes / an error index. This PR reworks the exception infrastructure in `libexpr` to support exception types with different constructor signatures than `BaseError`. Actually refactoring the exceptions to use structured data will come in a future PR (this one is big enough already, as it has to touch every exception in `libexpr`). The core design is in `eval-error.hh`. Generally, errors like this: state.error("'%s' is not a string", getAttrPathStr()) .debugThrow<TypeError>() are transformed like this: state.error<TypeError>("'%s' is not a string", getAttrPathStr()) .debugThrow() The type annotation has moved from `ErrorBuilder::debugThrow` to `EvalState::error`. (cherry picked from commit c6a89c1a1659b31694c0fbcd21d78a6dd521c732) Change-Id: Iced91ba4e00ca9e801518071fb43798936cbd05a
Diffstat (limited to 'src/libexpr/eval.cc')
-rw-r--r--src/libexpr/eval.cc217
1 files changed, 96 insertions, 121 deletions
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 98bf086ec..fac67950b 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -333,46 +333,6 @@ void initGC()
gcInitialised = true;
}
-
-ErrorBuilder & ErrorBuilder::atPos(PosIdx pos)
-{
- info.errPos = state.positions[pos];
- return *this;
-}
-
-ErrorBuilder & ErrorBuilder::withTrace(PosIdx pos, const std::string_view text)
-{
- info.traces.push_front(Trace{ .pos = state.positions[pos], .hint = hintformat(std::string(text)), .frame = false });
- return *this;
-}
-
-ErrorBuilder & ErrorBuilder::withFrameTrace(PosIdx pos, const std::string_view text)
-{
- info.traces.push_front(Trace{ .pos = state.positions[pos], .hint = hintformat(std::string(text)), .frame = true });
- return *this;
-}
-
-ErrorBuilder & ErrorBuilder::withSuggestions(Suggestions & s)
-{
- info.suggestions = s;
- return *this;
-}
-
-ErrorBuilder & ErrorBuilder::withFrame(const Env & env, const Expr & expr)
-{
- // NOTE: This is abusing side-effects.
- // TODO: check compatibility with nested debugger calls.
- state.debugTraces.push_front(DebugTrace {
- .pos = nullptr,
- .expr = expr,
- .env = env,
- .hint = hintformat("Fake frame for debugging purposes"),
- .isError = true
- });
- return *this;
-}
-
-
EvalState::EvalState(
const SearchPath & _searchPath,
ref<Store> store,
@@ -819,7 +779,7 @@ void EvalState::runDebugRepl(const Error * error, const Env & env, const Expr &
? std::make_unique<DebugTraceStacker>(
*this,
DebugTrace {
- .pos = error->info().errPos ? error->info().errPos : positions[expr.getPos()],
+ .pos = error->info().pos ? error->info().pos : positions[expr.getPos()],
.expr = expr,
.env = env,
.hint = error->info().msg,
@@ -936,7 +896,7 @@ inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval)
return j->value;
}
if (!fromWith->parentWith)
- error("undefined variable '%1%'", symbols[var.name]).atPos(var.pos).withFrame(*env, var).debugThrow<UndefinedVarError>();
+ error<UndefinedVarError>("undefined variable '%1%'", symbols[var.name]).atPos(var.pos).withFrame(*env, var).debugThrow();
for (size_t l = fromWith->prevWith; l; --l, env = env->up) ;
fromWith = fromWith->parentWith;
}
@@ -1162,7 +1122,7 @@ void EvalState::cacheFile(
// computation.
if (mustBeTrivial &&
!(dynamic_cast<ExprAttrs *>(e)))
- error("file '%s' must be an attribute set", path).debugThrow<EvalError>();
+ error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
eval(e, v);
} catch (Error & e) {
addErrorTrace(e, "while evaluating the file '%1%':", resolvedPath.to_string());
@@ -1186,10 +1146,11 @@ inline bool EvalState::evalBool(Env & env, Expr * e, const PosIdx pos, std::stri
Value v;
e->eval(*this, env, v);
if (v.type() != nBool)
- error("expected a Boolean but found %1%: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
- .withFrame(env, *e).debugThrow<TypeError>();
+ error<TypeError>(
+ "expected a Boolean but found %1%: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ ).atPos(pos).withFrame(env, *e).debugThrow();
return v.boolean;
} catch (Error & e) {
e.addTrace(positions[pos], errorCtx);
@@ -1203,10 +1164,11 @@ inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v, const PosIdx po
try {
e->eval(*this, env, v);
if (v.type() != nAttrs)
- error("expected a set but found %1%: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
- .withFrame(env, *e).debugThrow<TypeError>();
+ error<TypeError>(
+ "expected a set but found %1%: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ ).withFrame(env, *e).debugThrow();
} catch (Error & e) {
e.addTrace(positions[pos], errorCtx);
throw;
@@ -1315,7 +1277,7 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v)
auto nameSym = state.symbols.create(nameVal.string.s);
Bindings::iterator j = v.attrs->find(nameSym);
if (j != v.attrs->end())
- state.error("dynamic attribute '%1%' already defined at %2%", state.symbols[nameSym], state.positions[j->pos]).atPos(i.pos).withFrame(env, *this).debugThrow<EvalError>();
+ state.error<EvalError>("dynamic attribute '%1%' already defined at %2%", state.symbols[nameSym], state.positions[j->pos]).atPos(i.pos).withFrame(env, *this).debugThrow();
i.valueExpr->setName(nameSym);
/* Keep sorted order so find can catch duplicates */
@@ -1440,8 +1402,8 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v)
for (auto & attr : *vAttrs->attrs)
allAttrNames.insert(state.symbols[attr.name]);
auto suggestions = Suggestions::bestMatches(allAttrNames, state.symbols[name]);
- state.error("attribute '%1%' missing", state.symbols[name])
- .atPos(pos).withSuggestions(suggestions).withFrame(env, *this).debugThrow<EvalError>();
+ state.error<EvalError>("attribute '%1%' missing", state.symbols[name])
+ .atPos(pos).withSuggestions(suggestions).withFrame(env, *this).debugThrow();
}
}
vAttrs = j->value;
@@ -1514,7 +1476,7 @@ public:
void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & vRes, const PosIdx pos)
{
if (callDepth > evalSettings.maxCallDepth)
- error("stack overflow; max-call-depth exceeded").atPos(pos).template debugThrow<EvalError>();
+ error<EvalError>("stack overflow; max-call-depth exceeded").atPos(pos).debugThrow();
CallDepth _level(callDepth);
auto trace = evalSettings.traceFunctionCalls
@@ -1572,13 +1534,13 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
auto j = args[0]->attrs->get(i.name);
if (!j) {
if (!i.def) {
- error("function '%1%' called without required argument '%2%'",
+ error<TypeError>("function '%1%' called without required argument '%2%'",
(lambda.name ? std::string(symbols[lambda.name]) : "anonymous lambda"),
symbols[i.name])
.atPos(lambda.pos)
.withTrace(pos, "from call site")
.withFrame(*fun.lambda.env, lambda)
- .debugThrow<TypeError>();
+ .debugThrow();
}
env2.values[displ++] = i.def->maybeThunk(*this, env2);
} else {
@@ -1598,14 +1560,14 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
for (auto & formal : lambda.formals->formals)
formalNames.insert(symbols[formal.name]);
auto suggestions = Suggestions::bestMatches(formalNames, symbols[i.name]);
- error("function '%1%' called with unexpected argument '%2%'",
+ error<TypeError>("function '%1%' called with unexpected argument '%2%'",
(lambda.name ? std::string(symbols[lambda.name]) : "anonymous lambda"),
symbols[i.name])
.atPos(lambda.pos)
.withTrace(pos, "from call site")
.withSuggestions(suggestions)
.withFrame(*fun.lambda.env, lambda)
- .debugThrow<TypeError>();
+ .debugThrow();
}
abort(); // can't happen
}
@@ -1737,11 +1699,12 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
}
else
- error("attempt to call something which is not a function but %1%: %2%",
+ error<TypeError>(
+ "attempt to call something which is not a function but %1%: %2%",
showType(vCur),
ValuePrinter(*this, vCur, errorPrintOptions))
.atPos(pos)
- .debugThrow<TypeError>();
+ .debugThrow();
}
vRes = vCur;
@@ -1823,12 +1786,12 @@ void EvalState::autoCallFunction(Bindings & args, Value & fun, Value & res)
if (j != args.end()) {
attrs.insert(*j);
} else if (!i.def) {
- error(R"(cannot evaluate a function that has an argument without a value ('%1%')
+ error<MissingArgumentError>(R"(cannot evaluate a function that has an argument without a value ('%1%')
Nix attempted to evaluate a function as a top level expression; in
this case it must have its arguments supplied either by default
values, or passed explicitly with '--arg' or '--argstr'. See
https://nixos.org/manual/nix/stable/language/constructs.html#functions.)", symbols[i.name])
- .atPos(i.pos).withFrame(*fun.lambda.env, *fun.lambda.fun).debugThrow<MissingArgumentError>();
+ .atPos(i.pos).withFrame(*fun.lambda.env, *fun.lambda.fun).debugThrow();
}
}
}
@@ -1859,7 +1822,7 @@ void ExprAssert::eval(EvalState & state, Env & env, Value & v)
if (!state.evalBool(env, cond, pos, "in the condition of the assert statement")) {
std::ostringstream out;
cond->show(state.symbols, out);
- state.error("assertion '%1%' failed", out.str()).atPos(pos).withFrame(env, *this).debugThrow<AssertionError>();
+ state.error<AssertionError>("assertion '%1%' failed", out.str()).atPos(pos).withFrame(env, *this).debugThrow();
}
body->eval(state, env, v);
}
@@ -2037,14 +2000,14 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
nf = n;
nf += vTmp.fpoint;
} else
- state.error("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow<EvalError>();
+ state.error<EvalError>("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow();
} else if (firstType == nFloat) {
if (vTmp.type() == nInt) {
nf += vTmp.integer;
} else if (vTmp.type() == nFloat) {
nf += vTmp.fpoint;
} else
- state.error("cannot add %1% to a float", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow<EvalError>();
+ state.error<EvalError>("cannot add %1% to a float", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow();
} else {
if (s.empty()) s.reserve(es->size());
/* skip canonization of first path, which would only be not
@@ -2066,7 +2029,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
v.mkFloat(nf);
else if (firstType == nPath) {
if (!context.empty())
- state.error("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow<EvalError>();
+ state.error<EvalError>("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow();
v.mkPath(CanonPath(canonPath(str())));
} else
v.mkStringMove(c_str(), context);
@@ -2081,8 +2044,9 @@ void ExprPos::eval(EvalState & state, Env & env, Value & v)
void ExprBlackHole::eval(EvalState & state, Env & env, Value & v)
{
- state.error("infinite recursion encountered")
- .debugThrow<InfiniteRecursionError>();
+ state.error<InfiniteRecursionError>("infinite recursion encountered")
+ .atPos(v.determinePos(noPos))
+ .debugThrow();
}
// always force this to be separate, otherwise forceValue may inline it and take
@@ -2096,7 +2060,7 @@ void EvalState::tryFixupBlackHolePos(Value & v, PosIdx pos)
try {
std::rethrow_exception(e);
} catch (InfiniteRecursionError & e) {
- e.err.errPos = positions[pos];
+ e.atPos(positions[pos]);
} catch (...) {
}
}
@@ -2144,15 +2108,18 @@ NixInt EvalState::forceInt(Value & v, const PosIdx pos, std::string_view errorCt
try {
forceValue(v, pos);
if (v.type() != nInt)
- error("expected an integer but found %1%: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
- .debugThrow<TypeError>();
+ error<TypeError>(
+ "expected an integer but found %1%: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ ).atPos(pos).debugThrow();
return v.integer;
} catch (Error & e) {
e.addTrace(positions[pos], errorCtx);
throw;
}
+
+ return v.integer;
}
@@ -2163,10 +2130,11 @@ NixFloat EvalState::forceFloat(Value & v, const PosIdx pos, std::string_view err
if (v.type() == nInt)
return v.integer;
else if (v.type() != nFloat)
- error("expected a float but found %1%: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
- .debugThrow<TypeError>();
+ error<TypeError>(
+ "expected a float but found %1%: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ ).atPos(pos).debugThrow();
return v.fpoint;
} catch (Error & e) {
e.addTrace(positions[pos], errorCtx);
@@ -2180,15 +2148,18 @@ bool EvalState::forceBool(Value & v, const PosIdx pos, std::string_view errorCtx
try {
forceValue(v, pos);
if (v.type() != nBool)
- error("expected a Boolean but found %1%: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
- .debugThrow<TypeError>();
+ error<TypeError>(
+ "expected a Boolean but found %1%: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ ).atPos(pos).debugThrow();
return v.boolean;
} catch (Error & e) {
e.addTrace(positions[pos], errorCtx);
throw;
}
+
+ return v.boolean;
}
@@ -2203,10 +2174,11 @@ void EvalState::forceFunction(Value & v, const PosIdx pos, std::string_view erro
try {
forceValue(v, pos);
if (v.type() != nFunction && !isFunctor(v))
- error("expected a function but found %1%: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
- .debugThrow<TypeError>();
+ error<TypeError>(
+ "expected a function but found %1%: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ ).atPos(pos).debugThrow();
} catch (Error & e) {
e.addTrace(positions[pos], errorCtx);
throw;
@@ -2219,10 +2191,11 @@ std::string_view EvalState::forceString(Value & v, const PosIdx pos, std::string
try {
forceValue(v, pos);
if (v.type() != nString)
- error("expected a string but found %1%: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
- .debugThrow<TypeError>();
+ error<TypeError>(
+ "expected a string but found %1%: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ ).atPos(pos).debugThrow();
return v.string.s;
} catch (Error & e) {
e.addTrace(positions[pos], errorCtx);
@@ -2251,7 +2224,7 @@ std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos, std::s
{
auto s = forceString(v, pos, errorCtx);
if (v.string.context) {
- error("the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string.s, v.string.context[0]).withTrace(pos, errorCtx).debugThrow<EvalError>();
+ error<EvalError>("the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string.s, v.string.context[0]).withTrace(pos, errorCtx).debugThrow();
}
return s;
}
@@ -2316,11 +2289,13 @@ BackedStringView EvalState::coerceToString(
return std::move(*maybeString);
auto i = v.attrs->find(sOutPath);
if (i == v.attrs->end()) {
- error("cannot coerce %1% to a string: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
+ error<TypeError>(
+ "cannot coerce %1% to a string: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ )
.withTrace(pos, errorCtx)
- .debugThrow<TypeError>();
+ .debugThrow();
}
return coerceToString(pos, *i->value, context, errorCtx,
coerceMore, copyToStore, canonicalizePath);
@@ -2328,7 +2303,7 @@ BackedStringView EvalState::coerceToString(
if (v.type() == nExternal) {
try {
- return v.external->coerceToString(positions[pos], context, coerceMore, copyToStore);
+ return v.external->coerceToString(*this, pos, context, coerceMore, copyToStore);
} catch (Error & e) {
e.addTrace(nullptr, errorCtx);
throw;
@@ -2364,18 +2339,19 @@ BackedStringView EvalState::coerceToString(
}
}
- error("cannot coerce %1% to a string: %2%",
- showType(v),
- ValuePrinter(*this, v, errorPrintOptions))
+ error<TypeError>("cannot coerce %1% to a string: %2%",
+ showType(v),
+ ValuePrinter(*this, v, errorPrintOptions)
+ )
.withTrace(pos, errorCtx)
- .debugThrow<TypeError>();
+ .debugThrow();
}
StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePath & path)
{
if (nix::isDerivation(path.path.abs()))
- error("file names are not allowed to end in '%1%'", drvExtension).debugThrow<EvalError>();
+ error<EvalError>("file names are not allowed to end in '%1%'", drvExtension).debugThrow();
auto i = srcToStore.find(path);
@@ -2400,7 +2376,7 @@ SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext
{
auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned();
if (path == "" || path[0] != '/')
- error("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow<EvalError>();
+ error<EvalError>("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow();
return CanonPath(path);
}
@@ -2410,7 +2386,7 @@ StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringCon
auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned();
if (auto storePath = store->maybeParseStorePath(path))
return *storePath;
- error("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow<EvalError>();
+ error<EvalError>("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow();
}
@@ -2420,18 +2396,18 @@ std::pair<SingleDerivedPath, std::string_view> EvalState::coerceToSingleDerivedP
auto s = forceString(v, context, pos, errorCtx);
auto csize = context.size();
if (csize != 1)
- error(
+ error<EvalError>(
"string '%s' has %d entries in its context. It should only have exactly one entry",
s, csize)
- .withTrace(pos, errorCtx).debugThrow<EvalError>();
+ .withTrace(pos, errorCtx).debugThrow();
auto derivedPath = std::visit(overloaded {
[&](NixStringContextElem::Opaque && o) -> SingleDerivedPath {
return std::move(o);
},
[&](NixStringContextElem::DrvDeep &&) -> SingleDerivedPath {
- error(
+ error<EvalError>(
"string '%s' has a context which refers to a complete source and binary closure. This is not supported at this time",
- s).withTrace(pos, errorCtx).debugThrow<EvalError>();
+ s).withTrace(pos, errorCtx).debugThrow();
},
[&](NixStringContextElem::Built && b) -> SingleDerivedPath {
return std::move(b);
@@ -2454,16 +2430,16 @@ SingleDerivedPath EvalState::coerceToSingleDerivedPath(const PosIdx pos, Value &
error message. */
std::visit(overloaded {
[&](const SingleDerivedPath::Opaque & o) {
- error(
+ error<EvalError>(
"path string '%s' has context with the different path '%s'",
s, sExpected)
- .withTrace(pos, errorCtx).debugThrow<EvalError>();
+ .withTrace(pos, errorCtx).debugThrow();
},
[&](const SingleDerivedPath::Built & b) {
- error(
+ error<EvalError>(
"string '%s' has context with the output '%s' from derivation '%s', but the string is not the right placeholder for this derivation output. It should be '%s'",
s, b.output, b.drvPath->to_string(*store), sExpected)
- .withTrace(pos, errorCtx).debugThrow<EvalError>();
+ .withTrace(pos, errorCtx).debugThrow();
}
}, derivedPath.raw());
}
@@ -2545,7 +2521,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
case nThunk: // Must not be left by forceValue
default:
- error("cannot compare %1% with %2%", showType(v1), showType(v2)).withTrace(pos, errorCtx).debugThrow<EvalError>();
+ error<EvalError>("cannot compare %1% with %2%", showType(v1), showType(v2)).withTrace(pos, errorCtx).debugThrow();
}
}
@@ -2782,13 +2758,12 @@ SourcePath EvalState::findFile(const SearchPath & searchPath, const std::string_
if (hasPrefix(path, "nix/"))
return CanonPath(concatStrings(corepkgsPrefix, path.substr(4)));
- debugThrow(ThrownError({
- .msg = hintfmt(evalSettings.pureEval
+ error<ThrownError>(
+ evalSettings.pureEval
? "cannot look up '<%s>' in pure evaluation mode (use '--impure' to override)"
: "file '%s' was not found in the Nix search path (add it using $NIX_PATH or -I)",
- path),
- .errPos = positions[pos]
- }), 0, 0);
+ path
+ ).atPos(pos).debugThrow();
}
@@ -2858,11 +2833,11 @@ Expr * EvalState::parse(
}
-std::string ExternalValueBase::coerceToString(const Pos & pos, NixStringContext & context, bool copyMore, bool copyToStore) const
+std::string ExternalValueBase::coerceToString(EvalState & state, const PosIdx & pos, NixStringContext & context, bool copyMore, bool copyToStore) const
{
- throw TypeError({
- .msg = hintfmt("cannot coerce %1% to a string: %2%", showType(), *this)
- });
+ state.error<TypeError>(
+ "cannot coerce %1% to a string: %2%", showType(), *this
+ ).atPos(pos).debugThrow();
}