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
2 changes: 0 additions & 2 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,6 @@ StringSet NixRepl::completePrefix(const std::string & prefix)
// Quietly ignore parse errors.
} catch (EvalError & e) {
// Quietly ignore evaluation errors.
} catch (UndefinedVarError & e) {
// Quietly ignore undefined variable errors.
Comment on lines -425 to -426
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UndefinedVarError used to be a subclass of Error. This PR changes it to be a subclass of EvalError instead. There's already a catch clause for EvalError here, so the UndefinedVarError clause is no longer needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK sounds good.

} catch (BadURL & e) {
// Quietly ignore BadURL flake-related errors.
}
Expand Down
8 changes: 4 additions & 4 deletions src/libexpr/attr-path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ std::pair<Value *, PosIdx> findAlongAttrPath(EvalState & state, const std::strin
if (!attrIndex) {

if (v->type() != nAttrs)
throw TypeError(
state.error<TypeError>(
"the expression selected by the selection path '%1%' should be a set but is %2%",
attrPath,
showType(*v));
showType(*v)).debugThrow();
if (attr.empty())
throw Error("empty attribute name in selection path '%1%'", attrPath);

Expand All @@ -88,10 +88,10 @@ std::pair<Value *, PosIdx> findAlongAttrPath(EvalState & state, const std::strin
else {

if (!v->isList())
throw TypeError(
state.error<TypeError>(
"the expression selected by the selection path '%1%' should be a list but is %2%",
attrPath,
showType(*v));
showType(*v)).debugThrow();
if (*attrIndex >= v->listSize())
throw AttrPathNotFound("list index %1% in selection path '%2%' is out of range", *attrIndex, attrPath);

Expand Down
30 changes: 15 additions & 15 deletions src/libexpr/eval-cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ std::shared_ptr<AttrCursor> AttrCursor::maybeGetAttr(Symbol name, bool forceErro
if (forceErrors)
debug("reevaluating failed cached attribute '%s'", getAttrPathStr(name));
else
throw CachedEvalError("cached failure of attribute '%s'", getAttrPathStr(name));
throw CachedEvalError(root->state, "cached failure of attribute '%s'", getAttrPathStr(name));
} else
return std::make_shared<AttrCursor>(root,
std::make_pair(shared_from_this(), name), nullptr, std::move(attr));
Expand All @@ -500,15 +500,15 @@ std::shared_ptr<AttrCursor> AttrCursor::maybeGetAttr(Symbol name, bool forceErro
// evaluate to see whether 'name' exists
} else
return nullptr;
//throw TypeError("'%s' is not an attribute set", getAttrPathStr());
//error<TypeError>("'%s' is not an attribute set", getAttrPathStr()).debugThrow();
}
}

auto & v = forceValue();

if (v.type() != nAttrs)
return nullptr;
//throw TypeError("'%s' is not an attribute set", getAttrPathStr());
//error<TypeError>("'%s' is not an attribute set", getAttrPathStr()).debugThrow();

auto attr = v.attrs->get(name);

Expand Down Expand Up @@ -574,14 +574,14 @@ std::string AttrCursor::getString()
debug("using cached string attribute '%s'", getAttrPathStr());
return s->first;
} else
root->state.error("'%s' is not a string", getAttrPathStr()).debugThrow<TypeError>();
root->state.error<TypeError>("'%s' is not a string", getAttrPathStr()).debugThrow();
}
}

auto & v = forceValue();

if (v.type() != nString && v.type() != nPath)
root->state.error("'%s' is not a string but %s", getAttrPathStr()).debugThrow<TypeError>();
root->state.error<TypeError>("'%s' is not a string but %s", getAttrPathStr()).debugThrow();

return v.type() == nString ? v.c_str() : v.path().to_string();
}
Expand Down Expand Up @@ -616,7 +616,7 @@ string_t AttrCursor::getStringWithContext()
return *s;
}
} else
root->state.error("'%s' is not a string", getAttrPathStr()).debugThrow<TypeError>();
root->state.error<TypeError>("'%s' is not a string", getAttrPathStr()).debugThrow();
}
}

Expand All @@ -630,7 +630,7 @@ string_t AttrCursor::getStringWithContext()
else if (v.type() == nPath)
return {v.path().to_string(), {}};
else
root->state.error("'%s' is not a string but %s", getAttrPathStr()).debugThrow<TypeError>();
root->state.error<TypeError>("'%s' is not a string but %s", getAttrPathStr()).debugThrow();
}

bool AttrCursor::getBool()
Expand All @@ -643,14 +643,14 @@ bool AttrCursor::getBool()
debug("using cached Boolean attribute '%s'", getAttrPathStr());
return *b;
} else
root->state.error("'%s' is not a Boolean", getAttrPathStr()).debugThrow<TypeError>();
root->state.error<TypeError>("'%s' is not a Boolean", getAttrPathStr()).debugThrow();
}
}

auto & v = forceValue();

if (v.type() != nBool)
root->state.error("'%s' is not a Boolean", getAttrPathStr()).debugThrow<TypeError>();
root->state.error<TypeError>("'%s' is not a Boolean", getAttrPathStr()).debugThrow();

return v.boolean;
}
Expand All @@ -665,14 +665,14 @@ NixInt AttrCursor::getInt()
debug("using cached integer attribute '%s'", getAttrPathStr());
return i->x;
} else
throw TypeError("'%s' is not an integer", getAttrPathStr());
root->state.error<TypeError>("'%s' is not an integer", getAttrPathStr()).debugThrow();
}
}

auto & v = forceValue();

if (v.type() != nInt)
throw TypeError("'%s' is not an integer", getAttrPathStr());
root->state.error<TypeError>("'%s' is not an integer", getAttrPathStr()).debugThrow();

return v.integer;
}
Expand All @@ -687,7 +687,7 @@ std::vector<std::string> AttrCursor::getListOfStrings()
debug("using cached list of strings attribute '%s'", getAttrPathStr());
return *l;
} else
throw TypeError("'%s' is not a list of strings", getAttrPathStr());
root->state.error<TypeError>("'%s' is not a list of strings", getAttrPathStr()).debugThrow();
}
}

Expand All @@ -697,7 +697,7 @@ std::vector<std::string> AttrCursor::getListOfStrings()
root->state.forceValue(v, noPos);

if (v.type() != nList)
throw TypeError("'%s' is not a list", getAttrPathStr());
root->state.error<TypeError>("'%s' is not a list", getAttrPathStr()).debugThrow();

std::vector<std::string> res;

Expand All @@ -720,14 +720,14 @@ std::vector<Symbol> AttrCursor::getAttrs()
debug("using cached attrset attribute '%s'", getAttrPathStr());
return *attrs;
} else
root->state.error("'%s' is not an attribute set", getAttrPathStr()).debugThrow<TypeError>();
root->state.error<TypeError>("'%s' is not an attribute set", getAttrPathStr()).debugThrow();
}
}

auto & v = forceValue();

if (v.type() != nAttrs)
root->state.error("'%s' is not an attribute set", getAttrPathStr()).debugThrow<TypeError>();
root->state.error<TypeError>("'%s' is not an attribute set", getAttrPathStr()).debugThrow();

std::vector<Symbol> attrs;
for (auto & attr : *getValue().attrs)
Expand Down
113 changes: 113 additions & 0 deletions src/libexpr/eval-error.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#include "eval-error.hh"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question about why not just write the implementation in header file, maybe eval-error.hh?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduces compile times, at least to an extent. I can put the implementation in the header file if that's what's preferred, though.

Copy link
Member

@inclyc inclyc Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduces compile times, at least to an extent

If you only have implememtations in .cc files(formally translation unit, TUs), other libexpr users may COPY the source code again and do same instantiation.

Reduces compile times

IMHO, I think it is not a good option to do any 'optimization' without dedicated profiling. This may reduce compile time, but do make things hard somehow(as I commented above).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind this so much. If it becomes annoying it's easy to change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went to refactor this and remembered why I had it like this -- the PosIdx type is forward-declared (it comes from nixexpr.hh, which itself depends on eval-error.hh).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went to refactor this and remembered why I had it like this -- the PosIdx type is forward-declared (it comes from nixexpr.hh, which itself depends on eval-error.hh).

Hmm, so how about split PosIdx definition from nixexpr.hh , to resolve this circular #includes?

Maybe we can do this in next patches, for not just keep this as-is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately even if you move PosIdx out of nixexpr.hh, the definition of EvalErrorBuilder<T>::withFrame depends on EvalState :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately even if you move PosIdx out of nixexpr.hh, the definition of EvalErrorBuilder<T>::withFrame depends on EvalState :(

That's quite unfortunate. Ideally all headers are 'self-contained', looks like those many 'forward declaration's lead to such isssue. This cannot be trivially solved, then let's keep it as-is :(.

#include "eval.hh"
#include "value.hh"

namespace nix {

template<class T>
EvalErrorBuilder<T> & EvalErrorBuilder<T>::withExitStatus(unsigned int exitStatus)
{
error.withExitStatus(exitStatus);
return *this;
}

template<class T>
EvalErrorBuilder<T> & EvalErrorBuilder<T>::atPos(PosIdx pos)
{
error.err.pos = error.state.positions[pos];
return *this;
}

template<class T>
EvalErrorBuilder<T> & EvalErrorBuilder<T>::atPos(Value & value, PosIdx fallback)
{
return atPos(value.determinePos(fallback));
}

template<class T>
EvalErrorBuilder<T> & EvalErrorBuilder<T>::withTrace(PosIdx pos, const std::string_view text)
{
error.err.traces.push_front(
Trace{.pos = error.state.positions[pos], .hint = hintfmt(std::string(text)), .frame = false});
return *this;
}

template<class T>
EvalErrorBuilder<T> & EvalErrorBuilder<T>::withFrameTrace(PosIdx pos, const std::string_view text)
{
error.err.traces.push_front(
Trace{.pos = error.state.positions[pos], .hint = hintformat(std::string(text)), .frame = true});
return *this;
}

template<class T>
EvalErrorBuilder<T> & EvalErrorBuilder<T>::withSuggestions(Suggestions & s)
{
error.err.suggestions = s;
return *this;
}

template<class T>
EvalErrorBuilder<T> & EvalErrorBuilder<T>::withFrame(const Env & env, const Expr & expr)
{
// NOTE: This is abusing side-effects.
// TODO: check compatibility with nested debugger calls.
// TODO: What side-effects??
error.state.debugTraces.push_front(DebugTrace{
.pos = error.state.positions[expr.getPos()],
.expr = expr,
.env = env,
.hint = hintformat("Fake frame for debugging purposes"),
.isError = true});
return *this;
}

template<class T>
EvalErrorBuilder<T> & EvalErrorBuilder<T>::addTrace(PosIdx pos, hintformat hint, bool frame)
{
error.addTrace(error.state.positions[pos], hint, frame);
return *this;
}

template<class T>
template<typename... Args>
EvalErrorBuilder<T> &
EvalErrorBuilder<T>::addTrace(PosIdx pos, std::string_view formatString, const Args &... formatArgs)
{

addTrace(error.state.positions[pos], hintfmt(std::string(formatString), formatArgs...));
return *this;
}

template<class T>
void EvalErrorBuilder<T>::debugThrow()
{
if (error.state.debugRepl && !error.state.debugTraces.empty()) {
const DebugTrace & last = error.state.debugTraces.front();
const Env * env = &last.env;
const Expr * expr = &last.expr;
error.state.runDebugRepl(&error, *env, *expr);
}

// `EvalState` is the only class that can construct an `EvalErrorBuilder`,
// and it does so in dynamic storage. This is the final method called on
// any such instance and must delete itself before throwing the underlying
// error.
auto error = std::move(this->error);
delete this;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I see this is the delete corresponding to the new. @tfc is this legal?

Copy link
Member

@Ericson2314 Ericson2314 Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://isocpp.org/wiki/faq/freestore-mgmt#delete-this OK it appears to be.

Still, I am tempted to say we should be using std::unique and debugThrow can be a && method.


throw error;
}

template class EvalErrorBuilder<EvalError>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do this in .cc TUs, all instantiated classes must be listed here which may lead to extra maintenance issue.

template class EvalErrorBuilder<AssertionError>;
template class EvalErrorBuilder<ThrownError>;
template class EvalErrorBuilder<Abort>;
template class EvalErrorBuilder<TypeError>;
template class EvalErrorBuilder<UndefinedVarError>;
template class EvalErrorBuilder<MissingArgumentError>;
template class EvalErrorBuilder<InfiniteRecursionError>;
template class EvalErrorBuilder<CachedEvalError>;
template class EvalErrorBuilder<InvalidPathError>;
Comment on lines +102 to +111
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these all EvalError or subtypes of it? It would be nice to add some sort of static_assert to EvalErrorBuilder if so.

That would help make clear what exactly the "closed world' of instantiations that we need is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they're all subtypes of EvalError.

These are all the error types we use EvalErrorBuilder with. If they're not declared here it becomes a link error. The methods on EvalErrorBuilder require a state member on the error, so in practice it'll become a compile error to use EvalErrorBuilder with an error type that's not a subtype of EvalError.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK the state thing is good to hear.


}
104 changes: 104 additions & 0 deletions src/libexpr/eval-error.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#pragma once

#include <algorithm>

#include "error.hh"
#include "pos-idx.hh"

namespace nix {

struct Env;
struct Expr;
struct Value;

class EvalState;
template<class T>
class EvalErrorBuilder;

class EvalError : public Error
{
template<class T>
friend class EvalErrorBuilder;
public:
EvalState & state;

EvalError(EvalState & state, ErrorInfo && errorInfo)
: Error(errorInfo)
, state(state)
{
}

template<typename... Args>
explicit EvalError(EvalState & state, const std::string & formatString, const Args &... formatArgs)
: Error(formatString, formatArgs...)
, state(state)
{
}
};

MakeError(ParseError, Error);
MakeError(AssertionError, EvalError);
MakeError(ThrownError, AssertionError);
MakeError(Abort, EvalError);
MakeError(TypeError, EvalError);
MakeError(UndefinedVarError, EvalError);
MakeError(MissingArgumentError, EvalError);
MakeError(CachedEvalError, EvalError);
MakeError(InfiniteRecursionError, EvalError);

struct InvalidPathError : public EvalError
{
public:
Path path;
InvalidPathError(EvalState & state, const Path & path)
: EvalError(state, "path '%s' is not valid", path)
{
}
};

/**
* `EvalErrorBuilder`s may only be constructed by `EvalState`. The `debugThrow`
* method must be the final method in any such `EvalErrorBuilder` usage, and it
* handles deleting the object.
*/
template<class T>
class EvalErrorBuilder final
{
friend class EvalState;

template<typename... Args>
explicit EvalErrorBuilder(EvalState & state, const Args &... args)
: error(T(state, args...))
{
}

public:
T error;

[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> & withExitStatus(unsigned int exitStatus);

[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> & atPos(PosIdx pos);

[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> & atPos(Value & value, PosIdx fallback = noPos);

[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> & withTrace(PosIdx pos, const std::string_view text);

[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> & withFrameTrace(PosIdx pos, const std::string_view text);

[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> & withSuggestions(Suggestions & s);

[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> & withFrame(const Env & e, const Expr & ex);

[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> & addTrace(PosIdx pos, hintformat hint, bool frame = false);

template<typename... Args>
[[nodiscard, gnu::noinline]] EvalErrorBuilder<T> &
addTrace(PosIdx pos, std::string_view formatString, const Args &... formatArgs);

/**
* Delete the `EvalErrorBuilder` and throw the underlying exception.
*/
[[gnu::noinline, gnu::noreturn]] void debugThrow();
};

}
Loading