-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Towards structured error classes #9834
Conversation
a4fe892
to
673703e
Compare
8d0c4e2
to
bb7b162
Compare
@@ -0,0 +1,99 @@ | |||
#include "eval-error.hh" |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
).
There was a problem hiding this comment.
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 fromnixexpr.hh
, which itself depends oneval-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.
There was a problem hiding this comment.
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
:(
There was a problem hiding this comment.
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 ofnixexpr.hh
, the definition ofEvalErrorBuilder<T>::withFrame
depends onEvalState
:(
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 :(.
throw error; | ||
} | ||
|
||
template class EvalErrorBuilder<EvalError>; |
There was a problem hiding this comment.
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.
5a14403
to
6784cf4
Compare
4a301f3
to
3ec4cf3
Compare
template class EvalErrorBuilder<EvalError>; | ||
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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
} catch (UndefinedVarError & e) { | ||
// Quietly ignore undefined variable errors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK sounds good.
Sometimes |
Yes, this will make the debugger work in more places. |
I wrote the builder for the same reason as this PR, to simplify the complex code that existed before. That being said, the reason for that ugly code was that building error messages from a template using fmt/format reserves a lot of stack space for a code path that is seldom used. I think @pennae could elaborate more. So, the difficulty is to avoid allocating _any_ (or just one word?) stack space for the error message. This is why all error formatting was moved to noinline, noreturn functions.
My change reused a pointer to an error builder located in State. It was a bit ugly as the same memory was used by all error constructions, but hopefully they are never nested.
This PR reintroduces a stack-allocated error builder that performs formatting. Meaning that it probably allocates all the formatter memory on the stack too.
Now, I do not know how to test the performance impact, but at least you have the background to understand the reasons behind the previous code.
Le 30 janvier 2024 18:49:10 GMT+01:00, John Ericson ***@***.***> a écrit :
…In general, I like this :). But I would like to get the opinion of @pennae, and @layus (who wrote the original `ErrorBuilder`) too.
--
Reply to this email directly or view it on GitHub:
#9834 (comment)
You are receiving this because you were mentioned.
Message ID: ***@***.***>
|
I will say an ultimate goal with the structured errors stuff is to not do any formatting until the exception is rendered. The exception itself should just contain the raw structured data, no So while we might indeed have a problem in the short term --- thanks for pointing this out @layus --- I am confident the problem is gone in the final destination. |
Hmm. I agree this is a good idea but it will be challenging to implement. Currently the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise, I have nothing against this change.
https://github.com/ericson2314/nix/tree/more-structured-errors I started fixing this here. I think it would be good to pick up that work in conjunction with this. The basic idea was that the non-structured errors can still inherit from a type with the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks good :) would have been easier to go through if it had been split into more specific commits though.
there doesn't seem to be an eval perf impact from these changes, with or without nonline
s on the builder when built with LTO enabled (haven't tried with LTO turned off).
state.error("infinite recursion encountered") | ||
.debugThrow<InfiniteRecursionError>(); | ||
EvalErrorBuilder<InfiniteRecursionError>(state, "infinite recursion encountered") | ||
.atPos(v.determinePos(noPos)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since this position will ne overwritten anyway, is this needed?
src/libexpr/primops.cc
Outdated
.msg = hintfmt("memory limit exceeded by regular expression '%s'", re), | ||
.errPos = state.positions[pos] | ||
})); | ||
EvalErrorBuilder<EvalError>(state, "memory limit exceeded by regular expression '%s'", re) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unrelated nitpick: we're not sure it's wise to print regexes like this without escaping them as a nix string again first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately we have to retract our performance claims due to errors in the benchmarking setup, the PR as it stands today gives a rather massive 3.5% eval perf hit. that's not good and has to be rectified :(
3ec4cf3
to
30eda09
Compare
30eda09
to
58206ef
Compare
I've added a pre-allocated buffer to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we cannot reproduce nix search
being that much faster (it's still about 2% slower for us), but other evaluations are under 0.5% from the merge-base value. instead of placement new we should probably be using regular new as the error paths aren't hot enough that avoiding malloc will be enough of a benefit to justify the correctness implications.
(we're not a huge fan of the chained builder pattern here and would instead move all the builder methods into EvalError, give them all a &&
ref qualifiers, and wrap the throwing/debugRepl interaction into a macro to stick the whole construction into a noinline function. but that can just as well be future work)
While preparing PRs like NixOS#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`.
We're on C++ 20 now, we don't need this
58206ef
to
faaccec
Compare
Hi @pennae,
I may have asked before, but can you share the benchmark you use ? I think we should have benchmark "tests" or at least benchmark targets for the build. If you cannot or will not share it, I will consider writing some myself.
It feels wrong to use you as an oracle, and it feels odd to get performance feedback at the end of a long development process. It can bring frustration for those who do not know performance is such a strict constraint.
Le 31 janvier 2024 23:41:41 GMT+01:00, pennae ***@***.***> a écrit :
…
@pennae requested changes on this pull request.
unfortunately we have to retract our performance claims due to errors in the benchmarking setup, the PR as it stands today gives a rather massive 3.5% eval perf hit. that's not good and has to be rectified :(
--
Reply to this email directly or view it on GitHub:
#9834 (review)
You are receiving this because you were mentioned.
Message ID: ***@***.***>
|
In private correspondence, @pennae said:
|
we think we've described the method in detail but always avoided posting the exact inputs because we had never taken the time to clean our test system config up enough to post, but now we did. it's remarkably simple, and for best results run with cpu boosting disabled since that can introduce a bunch of uncertainty. the nixpkgs revision changes occasionally to keep up with the world, the only important thing for both nixpkgs and the system config is to remain stable (and recent/large enough) during a benchmark run. https://gist.github.com/pennae/43ebf7709d5e13ece3912e5233ce44d9 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
much better now! performance impacts on system eval are down in the noise now, nix search
is slightly slower but not enoguh to be bothered by it (less than 1%).
// any such instancve and must delete itself before throwing the underlying | ||
// error. | ||
auto error = std::move(this->error); | ||
delete this; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
This issue has been mentioned on NixOS Discourse. There might be relevant details there: https://discourse.nixos.org/t/tweag-nix-dev-update-54/39990/1 |
Motivation
While preparing PRs like #9753, I've had to change error messages in dozens of code paths. It would be nice if instead of
we could write
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 thanBaseError
. 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 inlibexpr
).Notes for reviewers
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:
EvalErrorBuilder<TypeError>(state, "'%s' is not a string", getAttrPathStr()) .debugThrow()
Priorities and Process
Add 👍 to pull requests you find important.
The Nix maintainer team uses a GitHub project board to schedule and track reviews.