[llvm-dev] [RFC] Error handling in LLVM libraries.

Lang Hames via llvm-dev
Wed, 10 Feb 2016 00:23:15 -0000

> > I don't think these are really independent. Whether or not you
need to
emit
> > a diagnostic depends on whether a caller can handle the corresponding
error,
> > which isn't something you know at the point where the error is
raised.

> But you do in the diag handler. For example, if you are trying to open
> multiple files, some of which are bitcode, you know to ignore
> InvalidBitcodeSignature.

That means anticipating the kinds of errors you do/don't want to recover
from at the point at which you insatiate your diagnostic handler. At that
point you may not have the information that you need to know whether you
want to recover. E.g. What if one path through library code can recover,
and another can't? Worse still, what if the divergence point between these
two paths is in library code itself? Now you need to thread two diagnostic
handlers up to that point. This scales very poorly.

Simply put: diagnostic handlers aren't library friendly. It's tough
enough
to get all libraries to agree on an error type, without having them all
agree on a diagnostic handlers too, and thread them everywhere.

> This highlights why I think it is important to keep diagnostics and
> errors separate. In the solution you have there is a need to allocate
> a std::string, even if that is never used.

Errors are only constructed on the error path. There is no construction on
the success path.

> If it was always a
> std::string it would not be that bad, but the framework seems designed
> to allow even more context to be stored, opening the way for fun cases
> of error handling interfering with lifetime management.

This is a real problem, but I think the solution is to have a style
guideline: You can't use reference types (in the general sense -
references, pointers, etc.) in errors. Errors that would otherwise require
references should have their error-message stringified at the point of
creation.

> > Consider someone who wants to use libObject to write an object-file
repair
> > tool: A "bad_symbol" error code tells you what went wrong,
but not
where or
> > why, so it's not much use in attempting a fix. On the other hand
"bad_symbol
> > { idx = 10 }" gives you enough information to pop up a dialog,
ask the
user
> > what the symbol at index 10 should be, then re-write that symbol table
> > entry.

> Using error results to find deep information you want to patch seems
> like a bad idea. A tool that wants to patch symbol indexes should be
> reading them directly.

I'm not sure I agree, but I don't have strong feelings on this
particular
example - it was intended as a straw man. My point is that error recover is
useful in general: there is a reason things like exceptions exist.
Diagnostic handlers are very poorly suited to general error recovery.

> > void check(TypedError Err) {
> >   if (!Err)
> >     return;
> >
> >   logAllUnhandledErrors(std::move(Err), errs(), "<tool
name>");
> >   exit(1);
> > }

> That is not the same that you get with a diagnostic handler. What you
> get is an exit after the error was propagated over the library layer,
> instead of an exit at the point where the issue is found.

As long as you log the error I'm not sure I see a meaningful difference? If
you're not debugging you shouldn't be crashing in your library. If you
are
debugging you want to set a breakpoint on the error constructor instead.

> I would prefer the diagnostic handler for exactly the same reason :-)
> Generality has a cost...

Could you explain what you see as the cost in this instance?

The scheme has a higher cost that std::error_code alone when you're
returning an error, but I don't think optimizing the error case is a
sensible thing to do. It has a potentially *lower* cost on the success
path, as you don't need to take up an argument for the diagnostic handler.

The most serious objection that you've raised, to my mind, is the potential
lifetime issues if people mistakenly use references in their error types.
I'm planning to write additions to llvm's coding guidelines to cover
that,
which I think should be sufficient.

On the flip side, compared to a diagnostic handler, I think the scheme I've
proposed has the following benefits:

- No need to thread a diagnostic handler type through the world (and
consequently no need to agree on its type). Retrofitting rich error logging
to an interface that already returns errors requires less work than adding
a diagnostic handler.
- The ability to describe error hierarchies (e.g. the class of all errors
that represent malformed objects, which may be further subclassed to
describe specific errors).
- The ability for client code to meaningfully distinguish between error
instances, rather than just error types.

> , and I don't think we should invest on it until
> we have a concrete case where that is needed.

The error infrastructure investment is largely done, and I'm volunteering
to maintain it. Myself and Kevin Enderby are signing up to weave this
through libObject and the JIT. Other libraries could be converted as people
see fit, with ECError providing an interface between the std::error_code
world and TypedError.

> Given that I would
> suggest going the more incremental way. Add a CheckedError that wraps
> error_code and use to make sure the errors are checked. When a better
> diagnostic is needed, pass in a diagnostic handler.

Our first use-case for this system is libObject, where we want to use this
to enable richer error messages. An incremental approach would involve
threading a diagnostic handler through everything (and returning
CheckedError), then un-threading it again would involve a lot of churn. I'd
prefer to move directly to the scheme I'm proposing.

Cheers,
Lang.


On Tue, Feb 9, 2016 at 3:27 PM, Rafael Espíndola <rafael.espindola at
gmail.com
> wrote:

> > I don't think these are really independent. Whether or not you
need to
> emit
> > a diagnostic depends on whether a caller can handle the corresponding
> error,
> > which isn't something you know at the point where the error is
raised.
>
> But you do in the diag handler. For example, if you are trying to open
> multiple files, some of which are bitcode, you know to ignore
> InvalidBitcodeSignature.
>
>
> > That's the idea behind the 'log' method on TypedErrorInfo:
It lets you
> > transform meaningful information about the problem into a log message
> > *after* you know whether it can be handled or not. Recoverable errors
> can be
> > logged (if the client wants to do so), but they don't have to be.
>
> ...
>
> > By contrast, in my system this would be:
> >
> > class InvalidBitcodeSignature :
TypedErrorInfo<InvalidBitcodeSignature>
> {};
> > class CorruptedBitcode : TypedErrorInfo<CorruptedBitcode> {
> > public:
> >   CorruptedBitcode(std::string Msg) : Msg(Msg) {}
> >   void log(raw_ostream &OS) const override { OS << Msg; }
> > private:
> >   std::string Msg;
> > };
>
> This highlights why I think it is important to keep diagnostics and
> errors separate. In the solution you have there is a need to allocate
> a std::string, even if that is never used. If it was always a
> std::string it would not be that bad, but the framework seems designed
> to allow even more context to be stored, opening the way for fun cases
> of error handling interfering with lifetime management.
>
> > Consider someone who wants to use libObject to write an object-file
> repair
> > tool: A "bad_symbol" error code tells you what went wrong,
but not where
> or
> > why, so it's not much use in attempting a fix. On the other hand
> "bad_symbol
> > { idx = 10 }" gives you enough information to pop up a dialog,
ask the
> user
> > what the symbol at index 10 should be, then re-write that symbol table
> > entry.
>
> Using error results to find deep information you want to patch seems
> like a bad idea. A tool that wants to patch symbol indexes should be
> reading them directly.
>
> >
> >> Other advantages are:
> >>
> >> * It is easy to use fatal error in simple programs by just using a
> >> diganostic handler that exits.
> >> * Very debugger friendly. It is easy to put a breakpoint at the
diag
> >> handler.
> >
> > This proposal provides the same advantages. I noted the second point
in
> the
> > original RFC. The first is easily implemented using an idiom I've
seen in
> > llvm-objdump and elsewhere:
> >
> > void check(TypedError Err) {
> >   if (!Err)
> >     return;
> >
> >   logAllUnhandledErrors(std::move(Err), errs(), "<tool
name>");
> >   exit(1);
> > }
>
> That is not the same that you get with a diagnostic handler. What you
> get is an exit after the error was propagated over the library layer,
> instead of an exit at the point where the issue is found.
>
> We use the above code now because lib/Object has no diagnostic handler.
>
> > Side-note: you can actually go one better and replace this idiom with:
> >
> > template <typename T>
> > T check(TypedErrorOr<T> ValOrErr) {
> >   if (!ValOrErr) {
> >     logAllUnhandledErrors(ValOrErr.takeError(), errs(), "<tool
name>");
> >     exit(1);
> >   }
> >   return std::move(*ValOrErr);
> > }
>
> Yes, we do pretty much that in ELF/COFF lld.
>
>
> > Using TypedError as a wrapper for std::error_code would prevent errors
> from
> > be silently dropped, which is a huge step forward. (Though TypedError
> would
> > be a misnomer - we'd need a new name. :)
>
> CheckedError?
>
> > Using a diagnostic handler would allow richer diagnostics from
libObject,
> > but it supports a narrower class of error-recovery than my proposal.
I'm
> > inclined to favor my proposal as a strictly more general solution.
>
> I would prefer the diagnostic handler for exactly the same reason :-)
> Generality has a cost, and I don't think we should invest on it until
> we have a concrete case where that is needed. Given that I would
> suggest going the more incremental way. Add a CheckedError that wraps
> error_code and use to make sure the errors are checked. When a better
> diagnostic is needed, pass in a diagnostic handler.
>
> If we ever get to the point were we really want to *return* more than
> an error_code, it will be a much smaller refactoring to
> s/CheckedError/TypedError/.
>
> Cheers,
> Rafael
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL:
<http://lists.llvm.org/pipermail/llvm-dev/attachments/20160209/3dddfdfc/attachment.html>