Lang Hames via llvm-dev
2016-Feb-03 01:29 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Hi All, I've been thinking lately about how to improve LLVM's error model and error reporting. A lack of good error reporting in Orc and MCJIT has forced me to spend a lot of time investigating hard-to-debug errors that could easily have been identified if we provided richer error information to the client, rather than just aborting. Kevin Enderby has made similar observations about the state of libObject and the difficulty of producing good error messages for damaged object files. I expect to encounter more issues like this as I continue work on the MachO side of LLD. I see tackling the error modeling problem as a first step towards improving error handling in general: if we make it easy to model errors, it may pave the way for better error handling in many parts of our libraries. At present in LLVM we model errors with std::error_code (and its helper, ErrorOr) and use diagnostic streams for error reporting. Neither of these seem entirely up to the job of providing a solid error-handling mechanism for library code. Diagnostic streams are great if all you want to do is report failure to the user and then terminate, but they can't be used to distinguish between different kinds of errors, and so are unsuited to many use-cases (especially error recovery). On the other hand, std::error_code allows error kinds to be distinguished, but suffers a number of drawbacks: 1. It carries no context: It tells you what went wrong, but not where or why, making it difficult to produce good diagnostics. 2. It's extremely easy to ignore or forget: instances can be silently dropped. 3. It's not especially debugger friendly: Most people call the error_code constructors directly for both success and failure values. Breakpoints have to be set carefully to avoid stopping when success values are constructed. In fairness to std::error_code, it has some nice properties too: 1. It's extremely lightweight. 2. It's explicit in the API (unlike exceptions). 3. It doesn't require C++ RTTI (a requirement for use in LLVM). To address these shortcomings I have prototyped a new error-handling scheme partially inspired by C++ exceptions. The aim was to preserve the performance and API visibility of std::error_code, while allowing users to define custom error classes and inheritance relationships between them. My hope is that library code could use this scheme to model errors in a meaningful way, allowing clients to inspect the error information and recover where possible, or provide a rich diagnostic when aborting. The scheme has three major "moving parts": 1. A new 'TypedError' class that can be used as a replacement for std::error_code. E.g. std::error_code foo(); becomes TypedError foo(); The TypedError class serves as a lightweight wrapper for the real error information (see (2)). It also contains a 'Checked' flag, initially set to false, that tracks whether the error has been handled or not. If a TypedError is ever destructed without being checked (or passed on to someone else) it will call std::terminate(). TypedError cannot be silently dropped. 2. A utility class, TypedErrorInfo, for building error class hierarchies rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. // Define a new error type implicitly inheriting from TypedErrorInfoBase. class MyCustomError : public TypedErrorInfo<MyCustomError> { public: // Custom error info. }; // Define a subclass of MyCustomError. class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, MyCustomError> { public: // Extends MyCustomError, adds new members. }; 3. A set of utility functions that use the custom RTTI system to inspect and handle typed errors. For example 'catchAllTypedErrors' and 'handleTypedError' cooperate to handle error instances in a type-safe way: TypedError foo() { if (SomeFailureCondition) return make_typed_error<MyCustomError>(); } TypedError Err = foo(); catchAllTypedErrors(std::move(Err), handleTypedError<MyCustomError>( [](std::unique_ptr<MyCustomError> E) { // Handle the error. return TypedError(); // <- Indicate success from handler. } ) ); If your initial reaction is "Too much boilerplate!" I understand, but take comfort: (1) In the overwhelmingly common case of simply returning errors, the usage is identical to std::error_code: if (TypedError Err = foo()) return Err; and (2) the boilerplate for catching errors is usually easily contained in a handful of utility functions, and tends not to crowd the rest of your source code. My initial experiments with this scheme involved updating many source lines, but did not add much code at all beyond the new error classes that were introduced. I believe that this scheme addresses many of the shortcomings of std::error_code while maintaining the strengths: 1. Context - Custom error classes enable the user to attach as much contextual information as desired. 2. Difficult to drop - The 'checked' flag in TypedError ensures that it can't be dropped, it must be explicitly "handled", even if that only involves catching the error and doing nothing. 3. Debugger friendly - You can set a breakpoint on any custom error class's constructor to catch that error being created. Since the error class hierarchy is rooted you can break on TypedErrorInfoBase::TypedErrorInfoBase to catch any error being raised. 4. Lightweight - Because TypedError instances are just a pointer and a checked-bit, move-constructing it is very cheap. We may also want to consider ignoring the 'checked' bit in release mode, at which point TypedError should be as cheap as std::error_code. 5. Explicit - TypedError is represented explicitly in the APIs, the same as std::error_code. 6. Does not require C++ RTTI - The custom RTTI system does not rely on any standard C++ RTTI features. This scheme also has one attribute that I haven't seen in previous error handling systems (though my experience in this area is limited): Errors are not copyable, due to ownership semantics of TypedError. I think this actually neatly captures the idea that there is a chain of responsibility for dealing with any given error. Responsibility may be transferred (e.g. by returning it to a caller), but it cannot be duplicated as it doesn't generally make sense for multiple people to report or attempt to recover from the same error. I've tested this prototype out by threading it through the object-creation APIs of libObject and using custom error classes to report errors in MachO headers. My initial experience is that this has enabled much richer error messages than are possible with std::error_code. To enable interaction with APIs that still use std::error_code I have added a custom ECError class that wraps a std::error_code, and can be converted back to a std::error_code using the typedErrorToErrorCode function. For now, all custom error code classes should (and do, in the prototype) derive from this utility class. In my experiments, this has made it easy to thread TypedError selectively through parts of the API. Eventually my hope is that TypedError could replace std::error_code for user-facing APIs, at which point custom errors would no longer need to derive from ECError, and ECError could be relegated to a utility for interacting with other codebases that still use std::error_code. So - I look forward to hearing your thoughts. :) Cheers, Lang. Attached files: typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds anchor() method to lib/Support/ErrorHandling.cpp). error_demo.tgz - Stand-alone program demo'ing basic use of the TypedError API. libobject_typed_error_demo.patch - Threads TypedError through the binary-file creation methods (createBinary, createObjectFile, etc). Proof-of-concept for how TypedError can be integrated into an existing system. -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/d9a1ecba/attachment-0001.html> -------------- next part -------------- A non-text attachment was scrubbed... Name: typed_error.patch Type: application/octet-stream Size: 22737 bytes Desc: not available URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/d9a1ecba/attachment-0002.obj> -------------- next part -------------- A non-text attachment was scrubbed... Name: error_demo.tgz Type: application/x-gzip Size: 1285 bytes Desc: not available URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/d9a1ecba/attachment-0001.bin> -------------- next part -------------- A non-text attachment was scrubbed... Name: thread_typederror_through_object_creation.patch Type: application/octet-stream Size: 104511 bytes Desc: not available URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/d9a1ecba/attachment-0003.obj>
Hal Finkel via llvm-dev
2016-Feb-03 01:55 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
----- Original Message -----> From: "Lang Hames via llvm-dev" <llvm-dev at lists.llvm.org> > To: "LLVM Developers Mailing List" <llvm-dev at lists.llvm.org> > Sent: Tuesday, February 2, 2016 7:29:45 PM > Subject: [llvm-dev] [RFC] Error handling in LLVM libraries. > > > > Hi All, > > > I've been thinking lately about how to improve LLVM's error model and > error reporting. A lack of good error reporting in Orc and MCJIT has > forced me to spend a lot of time investigating hard-to-debug errors > that could easily have been identified if we provided richer error > information to the client, rather than just aborting. Kevin Enderby > has made similar observations about the state of libObject and the > difficulty of producing good error messages for damaged object > files. I expect to encounter more issues like this as I continue > work on the MachO side of LLD. I see tackling the error modeling > problem as a first step towards improving error handling in general: > if we make it easy to model errors, it may pave the way for better > error handling in many parts of our libraries. > > > At present in LLVM we model errors with std::error_code (and its > helper, ErrorOr) and use diagnostic streams for error reporting. > Neither of these seem entirely up to the job of providing a solid > error-handling mechanism for library code. Diagnostic streams are > great if all you want to do is report failure to the user and then > terminate, but they can't be used to distinguish between different > kinds of errors, and so are unsuited to many use-cases (especially > error recovery). On the other hand, std::error_code allows error > kinds to be distinguished, but suffers a number of drawbacks: > > > 1. It carries no context: It tells you what went wrong, but not where > or why, making it difficult to produce good diagnostics.Generically, I like this idea. However, regarding context, I wonder about the best model. When we designed the diagnostic reporting interface (by which I mean the bits in include/llvm/IR/DiagnosticInfo.h and include/llvm/IR/DiagnosticPrinter.h), the ability to carry context was very important. There, however, because the objects are being passed via a callback to the user-installed handler, they can carry pointers/references to objects (Values, Functions, etc.) that will go away once the object that detected the error is destroyed. In the model you're proposing, all of the context must be contained within the error object itself (because, by the time the context is useful, an arbitrary amount of the call stack to the error-detection point has already been unwound). This greatly limits the amount of information that can be efficiently stored as context in the error object. Depending on the use cases, it might be better to pass the context to some kind of error-handler callback than to try to pack it all into a long-lived error object. Thoughts? Thanks again, Hal> 2. It's extremely easy to ignore or forget: instances can be silently > dropped. > 3. It's not especially debugger friendly: Most people call the > error_code constructors directly for both success and failure > values. Breakpoints have to be set carefully to avoid stopping when > success values are constructed. > > > In fairness to std::error_code, it has some nice properties too: > > > 1. It's extremely lightweight. > 2. It's explicit in the API (unlike exceptions). > 3. It doesn't require C++ RTTI (a requirement for use in LLVM). > > > To address these shortcomings I have prototyped a new error-handling > scheme partially inspired by C++ exceptions. The aim was to preserve > the performance and API visibility of std::error_code, while > allowing users to define custom error classes and inheritance > relationships between them. My hope is that library code could use > this scheme to model errors in a meaningful way, allowing clients to > inspect the error information and recover where possible, or provide > a rich diagnostic when aborting. > > > The scheme has three major "moving parts": > > > 1. A new 'TypedError' class that can be used as a replacement for > std::error_code. E.g. > > > std::error_code foo(); > > > becomes > > > TypedError foo(); > > > The TypedError class serves as a lightweight wrapper for the real > error information (see (2)). It also contains a 'Checked' flag, > initially set to false, that tracks whether the error has been > handled or not. If a TypedError is ever destructed without being > checked (or passed on to someone else) it will call > std::terminate(). TypedError cannot be silently dropped. > > > 2. A utility class, TypedErrorInfo, for building error class > hierarchies rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. > > > // Define a new error type implicitly inheriting from > TypedErrorInfoBase. > class MyCustomError : public TypedErrorInfo<MyCustomError> { > public: > // Custom error info. > }; > > > // Define a subclass of MyCustomError. > class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, > MyCustomError> { > public: > // Extends MyCustomError, adds new members. > }; > > > 3. A set of utility functions that use the custom RTTI system to > inspect and handle typed errors. For example 'catchAllTypedErrors' > and 'handleTypedError' cooperate to handle error instances in a > type-safe way: > > > TypedError foo() { > if (SomeFailureCondition) > return make_typed_error<MyCustomError>(); > } > > > TypedError Err = foo(); > > > catchAllTypedErrors(std::move(Err), > handleTypedError<MyCustomError>( > [](std::unique_ptr<MyCustomError> E) { > // Handle the error. > return TypedError(); // <- Indicate success from handler. > } > ) > ); > > > > > If your initial reaction is "Too much boilerplate!" I understand, but > take comfort: (1) In the overwhelmingly common case of simply > returning errors, the usage is identical to std::error_code: > > > if (TypedError Err = foo()) > return Err; > > > and (2) the boilerplate for catching errors is usually easily > contained in a handful of utility functions, and tends not to crowd > the rest of your source code. My initial experiments with this > scheme involved updating many source lines, but did not add much > code at all beyond the new error classes that were introduced. > > > > > I believe that this scheme addresses many of the shortcomings of > std::error_code while maintaining the strengths: > > > 1. Context - Custom error classes enable the user to attach as much > contextual information as desired. > > > 2. Difficult to drop - The 'checked' flag in TypedError ensures that > it can't be dropped, it must be explicitly "handled", even if that > only involves catching the error and doing nothing. > > > 3. Debugger friendly - You can set a breakpoint on any custom error > class's constructor to catch that error being created. Since the > error class hierarchy is rooted you can break on > TypedErrorInfoBase::TypedErrorInfoBase to catch any error being > raised. > > > 4. Lightweight - Because TypedError instances are just a pointer and > a checked-bit, move-constructing it is very cheap. We may also want > to consider ignoring the 'checked' bit in release mode, at which > point TypedError should be as cheap as std::error_code. > > > 5. Explicit - TypedError is represented explicitly in the APIs, the > same as std::error_code. > > > 6. Does not require C++ RTTI - The custom RTTI system does not rely > on any standard C++ RTTI features. > > > This scheme also has one attribute that I haven't seen in previous > error handling systems (though my experience in this area is > limited): Errors are not copyable, due to ownership semantics of > TypedError. I think this actually neatly captures the idea that > there is a chain of responsibility for dealing with any given error. > Responsibility may be transferred (e.g. by returning it to a > caller), but it cannot be duplicated as it doesn't generally make > sense for multiple people to report or attempt to recover from the > same error. > > > I've tested this prototype out by threading it through the > object-creation APIs of libObject and using custom error classes to > report errors in MachO headers. My initial experience is that this > has enabled much richer error messages than are possible with > std::error_code. > > > To enable interaction with APIs that still use std::error_code I have > added a custom ECError class that wraps a std::error_code, and can > be converted back to a std::error_code using the > typedErrorToErrorCode function. For now, all custom error code > classes should (and do, in the prototype) derive from this utility > class. In my experiments, this has made it easy to thread TypedError > selectively through parts of the API. Eventually my hope is that > TypedError could replace std::error_code for user-facing APIs, at > which point custom errors would no longer need to derive from > ECError, and ECError could be relegated to a utility for interacting > with other codebases that still use std::error_code. > > > So - I look forward to hearing your thoughts. :) > > > Cheers, > Lang. > > > > Attached files: > > > > typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds > anchor() method to lib/Support/ErrorHandling.cpp). > > > error_demo.tgz - Stand-alone program demo'ing basic use of the > TypedError API. > > > libobject_typed_error_demo.patch - Threads TypedError through the > binary-file creation methods (createBinary, createObjectFile, etc). > Proof-of-concept for how TypedError can be integrated into an > existing system. > > > _______________________________________________ > LLVM Developers mailing list > llvm-dev at lists.llvm.org > http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev >-- Hal Finkel Assistant Computational Scientist Leadership Computing Facility Argonne National Laboratory
James Y Knight via llvm-dev
2016-Feb-03 02:05 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Regarding one point in particular:> 2. Difficult to drop - The 'checked' flag in TypedError ensures that it > can't be dropped, it must be explicitly "handled", even if that only > involves catching the error and doing nothing.It seems to me that "[[clang::warn_unused_result]] class TypedError" is probably sufficient for ensuring people check a status return value; I'm not sure runtime checking really brings much additional value there. On Tue, Feb 2, 2016 at 8:29 PM, Lang Hames via llvm-dev < llvm-dev at lists.llvm.org> wrote:> Hi All, > > I've been thinking lately about how to improve LLVM's error model and > error reporting. A lack of good error reporting in Orc and MCJIT has forced > me to spend a lot of time investigating hard-to-debug errors that could > easily have been identified if we provided richer error information to the > client, rather than just aborting. Kevin Enderby has made similar > observations about the state of libObject and the difficulty of producing > good error messages for damaged object files. I expect to encounter more > issues like this as I continue work on the MachO side of LLD. I see > tackling the error modeling problem as a first step towards improving error > handling in general: if we make it easy to model errors, it may pave the > way for better error handling in many parts of our libraries. > > At present in LLVM we model errors with std::error_code (and its helper, > ErrorOr) and use diagnostic streams for error reporting. Neither of these > seem entirely up to the job of providing a solid error-handling mechanism > for library code. Diagnostic streams are great if all you want to do is > report failure to the user and then terminate, but they can't be used to > distinguish between different kinds of errors, and so are unsuited to many > use-cases (especially error recovery). On the other hand, std::error_code > allows error kinds to be distinguished, but suffers a number of drawbacks: > > 1. It carries no context: It tells you what went wrong, but not where or > why, making it difficult to produce good diagnostics. > 2. It's extremely easy to ignore or forget: instances can be silently > dropped. > 3. It's not especially debugger friendly: Most people call the error_code > constructors directly for both success and failure values. Breakpoints have > to be set carefully to avoid stopping when success values are constructed. > > In fairness to std::error_code, it has some nice properties too: > > 1. It's extremely lightweight. > 2. It's explicit in the API (unlike exceptions). > 3. It doesn't require C++ RTTI (a requirement for use in LLVM). > > To address these shortcomings I have prototyped a new error-handling > scheme partially inspired by C++ exceptions. The aim was to preserve the > performance and API visibility of std::error_code, while allowing users to > define custom error classes and inheritance relationships between them. My > hope is that library code could use this scheme to model errors in a > meaningful way, allowing clients to inspect the error information and > recover where possible, or provide a rich diagnostic when aborting. > > The scheme has three major "moving parts": > > 1. A new 'TypedError' class that can be used as a replacement for > std::error_code. E.g. > > std::error_code foo(); > > becomes > > TypedError foo(); > > The TypedError class serves as a lightweight wrapper for the real error > information (see (2)). It also contains a 'Checked' flag, initially set to > false, that tracks whether the error has been handled or not. If a > TypedError is ever destructed without being checked (or passed on to > someone else) it will call std::terminate(). TypedError cannot be silently > dropped. > > 2. A utility class, TypedErrorInfo, for building error class hierarchies > rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. > > // Define a new error type implicitly inheriting from TypedErrorInfoBase. > class MyCustomError : public TypedErrorInfo<MyCustomError> { > public: > // Custom error info. > }; > > // Define a subclass of MyCustomError. > class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, > MyCustomError> { > public: > // Extends MyCustomError, adds new members. > }; > > 3. A set of utility functions that use the custom RTTI system to inspect > and handle typed errors. For example 'catchAllTypedErrors' and > 'handleTypedError' cooperate to handle error instances in a type-safe way: > > TypedError foo() { > if (SomeFailureCondition) > return make_typed_error<MyCustomError>(); > } > > TypedError Err = foo(); > > catchAllTypedErrors(std::move(Err), > handleTypedError<MyCustomError>( > [](std::unique_ptr<MyCustomError> E) { > // Handle the error. > return TypedError(); // <- Indicate success from handler. > } > ) > ); > > > If your initial reaction is "Too much boilerplate!" I understand, but take > comfort: (1) In the overwhelmingly common case of simply returning errors, > the usage is identical to std::error_code: > > if (TypedError Err = foo()) > return Err; > > and (2) the boilerplate for catching errors is usually easily contained in > a handful of utility functions, and tends not to crowd the rest of your > source code. My initial experiments with this scheme involved updating many > source lines, but did not add much code at all beyond the new error classes > that were introduced. > > > I believe that this scheme addresses many of the shortcomings of > std::error_code while maintaining the strengths: > > 1. Context - Custom error classes enable the user to attach as much > contextual information as desired. > > 2. Difficult to drop - The 'checked' flag in TypedError ensures that it > can't be dropped, it must be explicitly "handled", even if that only > involves catching the error and doing nothing. > > 3. Debugger friendly - You can set a breakpoint on any custom error > class's constructor to catch that error being created. Since the error > class hierarchy is rooted you can break on > TypedErrorInfoBase::TypedErrorInfoBase to catch any error being raised. > > 4. Lightweight - Because TypedError instances are just a pointer and a > checked-bit, move-constructing it is very cheap. We may also want to > consider ignoring the 'checked' bit in release mode, at which point > TypedError should be as cheap as std::error_code. > > 5. Explicit - TypedError is represented explicitly in the APIs, the same > as std::error_code. > > 6. Does not require C++ RTTI - The custom RTTI system does not rely on any > standard C++ RTTI features. > > This scheme also has one attribute that I haven't seen in previous error > handling systems (though my experience in this area is limited): Errors are > not copyable, due to ownership semantics of TypedError. I think this > actually neatly captures the idea that there is a chain of responsibility > for dealing with any given error. Responsibility may be transferred (e.g. > by returning it to a caller), but it cannot be duplicated as it doesn't > generally make sense for multiple people to report or attempt to recover > from the same error. > > I've tested this prototype out by threading it through the object-creation > APIs of libObject and using custom error classes to report errors in MachO > headers. My initial experience is that this has enabled much richer error > messages than are possible with std::error_code. > > To enable interaction with APIs that still use std::error_code I have > added a custom ECError class that wraps a std::error_code, and can be > converted back to a std::error_code using the typedErrorToErrorCode > function. For now, all custom error code classes should (and do, in the > prototype) derive from this utility class. In my experiments, this has made > it easy to thread TypedError selectively through parts of the API. > Eventually my hope is that TypedError could replace std::error_code for > user-facing APIs, at which point custom errors would no longer need to > derive from ECError, and ECError could be relegated to a utility for > interacting with other codebases that still use std::error_code. > > So - I look forward to hearing your thoughts. :) > > Cheers, > Lang. > > Attached files: > > typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds > anchor() method to lib/Support/ErrorHandling.cpp). > > error_demo.tgz - Stand-alone program demo'ing basic use of the TypedError > API. > > libobject_typed_error_demo.patch - Threads TypedError through the > binary-file creation methods (createBinary, createObjectFile, etc). > Proof-of-concept for how TypedError can be integrated into an existing > system. > > > _______________________________________________ > LLVM Developers mailing list > llvm-dev at lists.llvm.org > http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev > >-------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/a4931048/attachment.html>
Lang Hames via llvm-dev
2016-Feb-03 02:23 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Hi James,> It seems to me that "[[clang::warn_unused_result]] class TypedError" is probably sufficient for ensuring people check a status return value; I'm not sure runtime checking really brings much additional value there.I see the attribute as complimentary. The runtime check provides a stronger guarantee: the error cannot be dropped on any path, rather than just "the result is used". The attribute can help you catch obvious violations of this at compile time. - Lang. Sent from my iPhone> On Feb 2, 2016, at 6:05 PM, James Y Knight <jyknight at google.com> wrote: > > Regarding one point in particular: >> 2. Difficult to drop - The 'checked' flag in TypedError ensures that it can't be dropped, it must be explicitly "handled", even if that only involves catching the error and doing nothing. > > It seems to me that "[[clang::warn_unused_result]] class TypedError" is probably sufficient for ensuring people check a status return value; I'm not sure runtime checking really brings much additional value there. > >> On Tue, Feb 2, 2016 at 8:29 PM, Lang Hames via llvm-dev <llvm-dev at lists.llvm.org> wrote: >> Hi All, >> >> I've been thinking lately about how to improve LLVM's error model and error reporting. A lack of good error reporting in Orc and MCJIT has forced me to spend a lot of time investigating hard-to-debug errors that could easily have been identified if we provided richer error information to the client, rather than just aborting. Kevin Enderby has made similar observations about the state of libObject and the difficulty of producing good error messages for damaged object files. I expect to encounter more issues like this as I continue work on the MachO side of LLD. I see tackling the error modeling problem as a first step towards improving error handling in general: if we make it easy to model errors, it may pave the way for better error handling in many parts of our libraries. >> >> At present in LLVM we model errors with std::error_code (and its helper, ErrorOr) and use diagnostic streams for error reporting. Neither of these seem entirely up to the job of providing a solid error-handling mechanism for library code. Diagnostic streams are great if all you want to do is report failure to the user and then terminate, but they can't be used to distinguish between different kinds of errors, and so are unsuited to many use-cases (especially error recovery). On the other hand, std::error_code allows error kinds to be distinguished, but suffers a number of drawbacks: >> >> 1. It carries no context: It tells you what went wrong, but not where or why, making it difficult to produce good diagnostics. >> 2. It's extremely easy to ignore or forget: instances can be silently dropped. >> 3. It's not especially debugger friendly: Most people call the error_code constructors directly for both success and failure values. Breakpoints have to be set carefully to avoid stopping when success values are constructed. >> >> In fairness to std::error_code, it has some nice properties too: >> >> 1. It's extremely lightweight. >> 2. It's explicit in the API (unlike exceptions). >> 3. It doesn't require C++ RTTI (a requirement for use in LLVM). >> >> To address these shortcomings I have prototyped a new error-handling scheme partially inspired by C++ exceptions. The aim was to preserve the performance and API visibility of std::error_code, while allowing users to define custom error classes and inheritance relationships between them. My hope is that library code could use this scheme to model errors in a meaningful way, allowing clients to inspect the error information and recover where possible, or provide a rich diagnostic when aborting. >> >> The scheme has three major "moving parts": >> >> 1. A new 'TypedError' class that can be used as a replacement for std::error_code. E.g. >> >> std::error_code foo(); >> >> becomes >> >> TypedError foo(); >> >> The TypedError class serves as a lightweight wrapper for the real error information (see (2)). It also contains a 'Checked' flag, initially set to false, that tracks whether the error has been handled or not. If a TypedError is ever destructed without being checked (or passed on to someone else) it will call std::terminate(). TypedError cannot be silently dropped. >> >> 2. A utility class, TypedErrorInfo, for building error class hierarchies rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. >> >> // Define a new error type implicitly inheriting from TypedErrorInfoBase. >> class MyCustomError : public TypedErrorInfo<MyCustomError> { >> public: >> // Custom error info. >> }; >> >> // Define a subclass of MyCustomError. >> class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, MyCustomError> { >> public: >> // Extends MyCustomError, adds new members. >> }; >> >> 3. A set of utility functions that use the custom RTTI system to inspect and handle typed errors. For example 'catchAllTypedErrors' and 'handleTypedError' cooperate to handle error instances in a type-safe way: >> >> TypedError foo() { >> if (SomeFailureCondition) >> return make_typed_error<MyCustomError>(); >> } >> >> TypedError Err = foo(); >> >> catchAllTypedErrors(std::move(Err), >> handleTypedError<MyCustomError>( >> [](std::unique_ptr<MyCustomError> E) { >> // Handle the error. >> return TypedError(); // <- Indicate success from handler. >> } >> ) >> ); >> >> >> If your initial reaction is "Too much boilerplate!" I understand, but take comfort: (1) In the overwhelmingly common case of simply returning errors, the usage is identical to std::error_code: >> >> if (TypedError Err = foo()) >> return Err; >> >> and (2) the boilerplate for catching errors is usually easily contained in a handful of utility functions, and tends not to crowd the rest of your source code. My initial experiments with this scheme involved updating many source lines, but did not add much code at all beyond the new error classes that were introduced. >> >> >> I believe that this scheme addresses many of the shortcomings of std::error_code while maintaining the strengths: >> >> 1. Context - Custom error classes enable the user to attach as much contextual information as desired. >> >> 2. Difficult to drop - The 'checked' flag in TypedError ensures that it can't be dropped, it must be explicitly "handled", even if that only involves catching the error and doing nothing. >> >> 3. Debugger friendly - You can set a breakpoint on any custom error class's constructor to catch that error being created. Since the error class hierarchy is rooted you can break on TypedErrorInfoBase::TypedErrorInfoBase to catch any error being raised. >> >> 4. Lightweight - Because TypedError instances are just a pointer and a checked-bit, move-constructing it is very cheap. We may also want to consider ignoring the 'checked' bit in release mode, at which point TypedError should be as cheap as std::error_code. >> >> 5. Explicit - TypedError is represented explicitly in the APIs, the same as std::error_code. >> >> 6. Does not require C++ RTTI - The custom RTTI system does not rely on any standard C++ RTTI features. >> >> This scheme also has one attribute that I haven't seen in previous error handling systems (though my experience in this area is limited): Errors are not copyable, due to ownership semantics of TypedError. I think this actually neatly captures the idea that there is a chain of responsibility for dealing with any given error. Responsibility may be transferred (e.g. by returning it to a caller), but it cannot be duplicated as it doesn't generally make sense for multiple people to report or attempt to recover from the same error. >> >> I've tested this prototype out by threading it through the object-creation APIs of libObject and using custom error classes to report errors in MachO headers. My initial experience is that this has enabled much richer error messages than are possible with std::error_code. >> >> To enable interaction with APIs that still use std::error_code I have added a custom ECError class that wraps a std::error_code, and can be converted back to a std::error_code using the typedErrorToErrorCode function. For now, all custom error code classes should (and do, in the prototype) derive from this utility class. In my experiments, this has made it easy to thread TypedError selectively through parts of the API. Eventually my hope is that TypedError could replace std::error_code for user-facing APIs, at which point custom errors would no longer need to derive from ECError, and ECError could be relegated to a utility for interacting with other codebases that still use std::error_code. >> >> So - I look forward to hearing your thoughts. :) >> >> Cheers, >> Lang. >> >> Attached files: >> >> typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds anchor() method to lib/Support/ErrorHandling.cpp). >> >> error_demo.tgz - Stand-alone program demo'ing basic use of the TypedError API. >> >> libobject_typed_error_demo.patch - Threads TypedError through the binary-file creation methods (createBinary, createObjectFile, etc). Proof-of-concept for how TypedError can be integrated into an existing system. >> >> >> _______________________________________________ >> LLVM Developers mailing list >> llvm-dev at lists.llvm.org >> http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev >-------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/ff63041f/attachment.html>
Mehdi Amini via llvm-dev
2016-Feb-03 02:33 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Hi Lang, I’m glad someone tackle this long lived issue :) I’ve started to think about it recently but didn’t as far as you did!> On Feb 2, 2016, at 5:29 PM, Lang Hames via llvm-dev <llvm-dev at lists.llvm.org> wrote: > > Hi All, > > I've been thinking lately about how to improve LLVM's error model and error reporting. A lack of good error reporting in Orc and MCJIT has forced me to spend a lot of time investigating hard-to-debug errors that could easily have been identified if we provided richer error information to the client, rather than just aborting. Kevin Enderby has made similar observations about the state of libObject and the difficulty of producing good error messages for damaged object files. I expect to encounter more issues like this as I continue work on the MachO side of LLD. I see tackling the error modeling problem as a first step towards improving error handling in general: if we make it easy to model errors, it may pave the way for better error handling in many parts of our libraries. > > At present in LLVM we model errors with std::error_code (and its helper, ErrorOr) and use diagnostic streams for error reporting. Neither of these seem entirely up to the job of providing a solid error-handling mechanism for library code. Diagnostic streams are great if all you want to do is report failure to the user and then terminate, but they can't be used to distinguish between different kinds of errorsI’m not sure to understand this claim? You are supposed to be able to extend and subclass the type of diagnostics? (I remember doing it for an out-of-tree LLVM-based project).> , and so are unsuited to many use-cases (especially error recovery). On the other hand, std::error_code allows error kinds to be distinguished, but suffers a number of drawbacks: > > 1. It carries no context: It tells you what went wrong, but not where or why, making it difficult to produce good diagnostics. > 2. It's extremely easy to ignore or forget: instances can be silently dropped. > 3. It's not especially debugger friendly: Most people call the error_code constructors directly for both success and failure values. Breakpoints have to be set carefully to avoid stopping when success values are constructed. > > In fairness to std::error_code, it has some nice properties too: > > 1. It's extremely lightweight. > 2. It's explicit in the API (unlike exceptions). > 3. It doesn't require C++ RTTI (a requirement for use in LLVM). > > To address these shortcomings I have prototyped a new error-handling scheme partially inspired by C++ exceptions. The aim was to preserve the performance and API visibility of std::error_code, while allowing users to define custom error classes and inheritance relationships between them. My hope is that library code could use this scheme to model errors in a meaningful way, allowing clients to inspect the error information and recover where possible, or provide a rich diagnostic when aborting. > > The scheme has three major "moving parts": > > 1. A new 'TypedError' class that can be used as a replacement for std::error_code. E.g. > > std::error_code foo(); > > becomes > > TypedError foo(); > > The TypedError class serves as a lightweight wrapper for the real error information (see (2)). It also contains a 'Checked' flag, initially set to false, that tracks whether the error has been handled or not. If a TypedError is ever destructed without being checked (or passed on to someone else) it will call std::terminate(). TypedError cannot be silently dropped.I really like the fact that not checking the error triggers an error (this is the "hard to misuse” part of API design IMO). You don’t mention it, but I’d rather see this “checked” flag compiled out with NDEBUG.> > 2. A utility class, TypedErrorInfo, for building error class hierarchies rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. > > // Define a new error type implicitly inheriting from TypedErrorInfoBase. > class MyCustomError : public TypedErrorInfo<MyCustomError> { > public: > // Custom error info. > }; > > // Define a subclass of MyCustomError. > class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, MyCustomError> { > public: > // Extends MyCustomError, adds new members. > }; > > 3. A set of utility functions that use the custom RTTI system to inspect and handle typed errors. For example 'catchAllTypedErrors' and 'handleTypedError' cooperate to handle error instances in a type-safe way: > > TypedError foo() { > if (SomeFailureCondition) > return make_typed_error<MyCustomError>(); > } > > TypedError Err = foo(); > > catchAllTypedErrors(std::move(Err), > handleTypedError<MyCustomError>( > [](std::unique_ptr<MyCustomError> E) { > // Handle the error. > return TypedError(); // <- Indicate success from handler.What does success or failure means for the handler?> } > ) > ); > > > If your initial reaction is "Too much boilerplate!" I understand, but take comfort: (1) In the overwhelmingly common case of simply returning errors, the usage is identical to std::error_code: > > if (TypedError Err = foo()) > return Err; > > and (2) the boilerplate for catching errors is usually easily contained in a handful of utility functions, and tends not to crowd the rest of your source code. My initial experiments with this scheme involved updating many source lines, but did not add much code at all beyond the new error classes that were introduced. > > > I believe that this scheme addresses many of the shortcomings of std::error_code while maintaining the strengths: > > 1. Context - Custom error classes enable the user to attach as much contextual information as desired. > > 2. Difficult to drop - The 'checked' flag in TypedError ensures that it can't be dropped, it must be explicitly "handled", even if that only involves catching the error and doing nothing. > > 3. Debugger friendly - You can set a breakpoint on any custom error class's constructor to catch that error being created. Since the error class hierarchy is rooted you can break on TypedErrorInfoBase::TypedErrorInfoBase to catch any error being raised. > > 4. Lightweight - Because TypedError instances are just a pointer and a checked-bit, move-constructing it is very cheap. We may also want to consider ignoring the 'checked' bit in release mode, at which point TypedError should be as cheap as std::error_code.Oh here you mention compiling out the “checked” flag :)> > 5. Explicit - TypedError is represented explicitly in the APIs, the same as std::error_code. > > 6. Does not require C++ RTTI - The custom RTTI system does not rely on any standard C++ RTTI features. > > This scheme also has one attribute that I haven't seen in previous error handling systems (though my experience in this area is limited): Errors are not copyable, due to ownership semantics of TypedError. I think this actually neatly captures the idea that there is a chain of responsibility for dealing with any given error. Responsibility may be transferred (e.g. by returning it to a caller), but it cannot be duplicated as it doesn't generally make sense for multiple people to report or attempt to recover from the same error. > > I've tested this prototype out by threading it through the object-creation APIs of libObject and using custom error classes to report errors in MachO headers. My initial experience is that this has enabled much richer error messages than are possible with std::error_code. > > To enable interaction with APIs that still use std::error_code I have added a custom ECError class that wraps a std::error_code, and can be converted back to a std::error_code using the typedErrorToErrorCode function. For now, all custom error code classes should (and do, in the prototype) derive from this utility class. In my experiments, this has made it easy to thread TypedError selectively through parts of the API. Eventually my hope is that TypedError could replace std::error_code for user-facing APIs, at which point custom errors would no longer need to derive from ECError, and ECError could be relegated to a utility for interacting with other codebases that still use std::error_code. > > So - I look forward to hearing your thoughts. :)Is your call to catchAllTypedErrors(…) actually like a switch on the type of the error? What about a syntax that looks like a switch? switchErr(std::move(Err)) .case< MyCustomError>([] () { /* … */ }) .case< MyOtherCustomError>([] () { /* … */ }) .default([] () { /* … */ }) — Mehdi> > Cheers, > Lang. > > Attached files: > > typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds anchor() method to lib/Support/ErrorHandling.cpp). > > error_demo.tgz - Stand-alone program demo'ing basic use of the TypedError API. > > libobject_typed_error_demo.patch - Threads TypedError through the binary-file creation methods (createBinary, createObjectFile, etc). Proof-of-concept for how TypedError can be integrated into an existing system. > > <typed_error.patch><error_demo.tgz><thread_typederror_through_object_creation.patch>_______________________________________________ > LLVM Developers mailing list > llvm-dev at lists.llvm.org > http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev-------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/7f3a8798/attachment-0001.html>
Lang Hames via llvm-dev
2016-Feb-03 04:11 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Hi Hal, However, regarding context, I wonder about the best model. When we designed> the diagnostic reporting interface (by which I mean the bits in > include/llvm/IR/DiagnosticInfo.h and include/llvm/IR/DiagnosticPrinter.h), > the ability to carry context was very important. There, however, because > the objects are being passed via a callback to the user-installed handler, > they can carry pointers/references to objects (Values, Functions, etc.) > that will go away once the object that detected the error is destroyed. In > the model you're proposing, all of the context must be contained within the > error object itself (because, by the time the context is useful, an > arbitrary amount of the call stack to the error-detection point has already > been unwound). This greatly limits the amount of information that can be > efficiently stored as context in the error object. Depending on the use > cases, it might be better to pass the context to some kind of error-handler > callback than to try to pack it all into a long-lived error object. > Thoughts?I think this is one of the trickiest problems when designing an error return for C++. In garbage-collected languages you can attach all sorts of useful things and the reference from the error value will keep them alive. In C++ any non-owning reference or pointer type could be pointing into space by the time you reach the error handler (from the library's point of view). I think the best you can do here is document the pitfalls and provide guidelines for designing error types. Kevin and I noted the following two relevant guidelines while discussing exactly this problem: (1) Errors should not contain non-owning references or pointers. (Preferably, errors should only contain value types) (2) New error types should only be introduced to model errors than clients could conceivably recover from, or where different clients may want to format the error messages differently. Any error that is only useful for diagnostic purposes should probably use a class along the lines of: class DiagnosticError ... { std::string Msg; void log(ostream &os) { os << Msg; } }; Given the parallels with exceptions, I suspect many of the same design guidelines would apply here. I'm also not averse to mixing diagnostic streams with my system where they make sense - I think it's always a matter of choosing the right tool for the job. I just need a solution for the error recovery problem, and diagnostic streams don't provide one. :) Cheers, Lang. On Tue, Feb 2, 2016 at 5:55 PM, Hal Finkel <hfinkel at anl.gov> wrote:> ----- Original Message ----- > > From: "Lang Hames via llvm-dev" <llvm-dev at lists.llvm.org> > > To: "LLVM Developers Mailing List" <llvm-dev at lists.llvm.org> > > Sent: Tuesday, February 2, 2016 7:29:45 PM > > Subject: [llvm-dev] [RFC] Error handling in LLVM libraries. > > > > > > > > Hi All, > > > > > > I've been thinking lately about how to improve LLVM's error model and > > error reporting. A lack of good error reporting in Orc and MCJIT has > > forced me to spend a lot of time investigating hard-to-debug errors > > that could easily have been identified if we provided richer error > > information to the client, rather than just aborting. Kevin Enderby > > has made similar observations about the state of libObject and the > > difficulty of producing good error messages for damaged object > > files. I expect to encounter more issues like this as I continue > > work on the MachO side of LLD. I see tackling the error modeling > > problem as a first step towards improving error handling in general: > > if we make it easy to model errors, it may pave the way for better > > error handling in many parts of our libraries. > > > > > > At present in LLVM we model errors with std::error_code (and its > > helper, ErrorOr) and use diagnostic streams for error reporting. > > Neither of these seem entirely up to the job of providing a solid > > error-handling mechanism for library code. Diagnostic streams are > > great if all you want to do is report failure to the user and then > > terminate, but they can't be used to distinguish between different > > kinds of errors, and so are unsuited to many use-cases (especially > > error recovery). On the other hand, std::error_code allows error > > kinds to be distinguished, but suffers a number of drawbacks: > > > > > > 1. It carries no context: It tells you what went wrong, but not where > > or why, making it difficult to produce good diagnostics. > > Generically, I like this idea. > > However, regarding context, I wonder about the best model. When we > designed the diagnostic reporting interface (by which I mean the bits in > include/llvm/IR/DiagnosticInfo.h and include/llvm/IR/DiagnosticPrinter.h), > the ability to carry context was very important. There, however, because > the objects are being passed via a callback to the user-installed handler, > they can carry pointers/references to objects (Values, Functions, etc.) > that will go away once the object that detected the error is destroyed. In > the model you're proposing, all of the context must be contained within the > error object itself (because, by the time the context is useful, an > arbitrary amount of the call stack to the error-detection point has already > been unwound). This greatly limits the amount of information that can be > efficiently stored as context in the error object. Depending on the use > cases, it might be better to pass the context to some kind of error-handler > callback than to try to pack it all into a long-lived error object. > Thoughts? > > Thanks again, > Hal > > > 2. It's extremely easy to ignore or forget: instances can be silently > > dropped. > > 3. It's not especially debugger friendly: Most people call the > > error_code constructors directly for both success and failure > > values. Breakpoints have to be set carefully to avoid stopping when > > success values are constructed. > > > > > > In fairness to std::error_code, it has some nice properties too: > > > > > > 1. It's extremely lightweight. > > 2. It's explicit in the API (unlike exceptions). > > 3. It doesn't require C++ RTTI (a requirement for use in LLVM). > > > > > > To address these shortcomings I have prototyped a new error-handling > > scheme partially inspired by C++ exceptions. The aim was to preserve > > the performance and API visibility of std::error_code, while > > allowing users to define custom error classes and inheritance > > relationships between them. My hope is that library code could use > > this scheme to model errors in a meaningful way, allowing clients to > > inspect the error information and recover where possible, or provide > > a rich diagnostic when aborting. > > > > > > The scheme has three major "moving parts": > > > > > > 1. A new 'TypedError' class that can be used as a replacement for > > std::error_code. E.g. > > > > > > std::error_code foo(); > > > > > > becomes > > > > > > TypedError foo(); > > > > > > The TypedError class serves as a lightweight wrapper for the real > > error information (see (2)). It also contains a 'Checked' flag, > > initially set to false, that tracks whether the error has been > > handled or not. If a TypedError is ever destructed without being > > checked (or passed on to someone else) it will call > > std::terminate(). TypedError cannot be silently dropped. > > > > > > 2. A utility class, TypedErrorInfo, for building error class > > hierarchies rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. > > > > > > // Define a new error type implicitly inheriting from > > TypedErrorInfoBase. > > class MyCustomError : public TypedErrorInfo<MyCustomError> { > > public: > > // Custom error info. > > }; > > > > > > // Define a subclass of MyCustomError. > > class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, > > MyCustomError> { > > public: > > // Extends MyCustomError, adds new members. > > }; > > > > > > 3. A set of utility functions that use the custom RTTI system to > > inspect and handle typed errors. For example 'catchAllTypedErrors' > > and 'handleTypedError' cooperate to handle error instances in a > > type-safe way: > > > > > > TypedError foo() { > > if (SomeFailureCondition) > > return make_typed_error<MyCustomError>(); > > } > > > > > > TypedError Err = foo(); > > > > > > catchAllTypedErrors(std::move(Err), > > handleTypedError<MyCustomError>( > > [](std::unique_ptr<MyCustomError> E) { > > // Handle the error. > > return TypedError(); // <- Indicate success from handler. > > } > > ) > > ); > > > > > > > > > > If your initial reaction is "Too much boilerplate!" I understand, but > > take comfort: (1) In the overwhelmingly common case of simply > > returning errors, the usage is identical to std::error_code: > > > > > > if (TypedError Err = foo()) > > return Err; > > > > > > and (2) the boilerplate for catching errors is usually easily > > contained in a handful of utility functions, and tends not to crowd > > the rest of your source code. My initial experiments with this > > scheme involved updating many source lines, but did not add much > > code at all beyond the new error classes that were introduced. > > > > > > > > > > I believe that this scheme addresses many of the shortcomings of > > std::error_code while maintaining the strengths: > > > > > > 1. Context - Custom error classes enable the user to attach as much > > contextual information as desired. > > > > > > 2. Difficult to drop - The 'checked' flag in TypedError ensures that > > it can't be dropped, it must be explicitly "handled", even if that > > only involves catching the error and doing nothing. > > > > > > 3. Debugger friendly - You can set a breakpoint on any custom error > > class's constructor to catch that error being created. Since the > > error class hierarchy is rooted you can break on > > TypedErrorInfoBase::TypedErrorInfoBase to catch any error being > > raised. > > > > > > 4. Lightweight - Because TypedError instances are just a pointer and > > a checked-bit, move-constructing it is very cheap. We may also want > > to consider ignoring the 'checked' bit in release mode, at which > > point TypedError should be as cheap as std::error_code. > > > > > > 5. Explicit - TypedError is represented explicitly in the APIs, the > > same as std::error_code. > > > > > > 6. Does not require C++ RTTI - The custom RTTI system does not rely > > on any standard C++ RTTI features. > > > > > > This scheme also has one attribute that I haven't seen in previous > > error handling systems (though my experience in this area is > > limited): Errors are not copyable, due to ownership semantics of > > TypedError. I think this actually neatly captures the idea that > > there is a chain of responsibility for dealing with any given error. > > Responsibility may be transferred (e.g. by returning it to a > > caller), but it cannot be duplicated as it doesn't generally make > > sense for multiple people to report or attempt to recover from the > > same error. > > > > > > I've tested this prototype out by threading it through the > > object-creation APIs of libObject and using custom error classes to > > report errors in MachO headers. My initial experience is that this > > has enabled much richer error messages than are possible with > > std::error_code. > > > > > > To enable interaction with APIs that still use std::error_code I have > > added a custom ECError class that wraps a std::error_code, and can > > be converted back to a std::error_code using the > > typedErrorToErrorCode function. For now, all custom error code > > classes should (and do, in the prototype) derive from this utility > > class. In my experiments, this has made it easy to thread TypedError > > selectively through parts of the API. Eventually my hope is that > > TypedError could replace std::error_code for user-facing APIs, at > > which point custom errors would no longer need to derive from > > ECError, and ECError could be relegated to a utility for interacting > > with other codebases that still use std::error_code. > > > > > > So - I look forward to hearing your thoughts. :) > > > > > > Cheers, > > Lang. > > > > > > > > Attached files: > > > > > > > > typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds > > anchor() method to lib/Support/ErrorHandling.cpp). > > > > > > error_demo.tgz - Stand-alone program demo'ing basic use of the > > TypedError API. > > > > > > libobject_typed_error_demo.patch - Threads TypedError through the > > binary-file creation methods (createBinary, createObjectFile, etc). > > Proof-of-concept for how TypedError can be integrated into an > > existing system. > > > > > > _______________________________________________ > > LLVM Developers mailing list > > llvm-dev at lists.llvm.org > > http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev > > > > -- > Hal Finkel > Assistant Computational Scientist > Leadership Computing Facility > Argonne National Laboratory >-------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/25c610d8/attachment.html>
Lang Hames via llvm-dev
2016-Feb-03 06:42 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Hi Mehdi,> I’m not sure to understand this claim? You are supposed to be able toextend and subclass the type of diagnostics? (I remember doing it for an out-of-tree LLVM-based project). You can subclass diagnostics, but subclassing (on its own) only lets you change the behaviour of the diagnostic/error itself. What we need, and what this patch supplies, is a way to choose a particular handler based on the type of the error. For that you need RTTI, so this patch introduces a new RTTI scheme that I think is more suitable for errors types*, since unlike LLVM's existing RTTI system it doesn't require you to enumerate the types up-front. * If this RTTI system is considered generically useful it could be split out into its own utility. It's slightly higher cost than LLVM's system: One byte of BSS per type, and a walk from the dynamic type of the error to the root of the type-hierarchy (with possible early exit) for each type check.> What does success or failure means for the handler?It gives the handler an opportunity to inspect and then "re-throw" an error if necessary: A handler might not know whether it can recover based on type alone, or it may not want to recover at all, but instead attach some context to provide a richer diagnostic. As a concrete example, one of our motivating cases is processing object files in archives. Down in the object file processing code, a load command might be found to be malformed, but at that point there's no context to tell us that the object that it's in is part of an archive, so the best diagnostic we could produce is "In foo.o: malformed load command at index N". A (straw-man) improved system might look like this: class ObjectError ... { // <- Root of all object-file errors std::string ArchiveName = ""; std::string ObjectName = ""; std::error_code EC; void log(raw_ostream &OS) const override { if (!ArchiveName.empty()) OS << "In archive '" << ArchiveName << "', "; OS << "In object file '" << ObjectName << "', " << EC.message(); } }; TypedError processArchive(Archive &A) { TypedError Err; for (auto &Obj : A) { auto Err = processObject(Obj); if (auto E2 catchTypedErrors(std::move(Err), handleTypedError<ObjectError>([&](std::unique_ptr<ObjectError> OE) { OE->ArchiveName = A.getName(); return TypedError(std::move(OE)); })) return E2; } } In this example, any error (whether an ObjectError or something else) will be intercepted by the 'catchTypedErrors' function. If the error *isn't* an ObjectError it'll be returned unmodified out of catchTypedErrors, triggering an immediate return from processArchive. If it *is* an ObjectError then the handler will be run, giving us an opportunity to tag the error as having occurred within archive A. Again - this is a straw-man example: I think we can do better again for diagnostics of this kind, but it showcases the value of being able to modify errors while they're in-flight.> Is your call to catchAllTypedErrors(…) actually like a switch on the typeof the error? What about a syntax that looks like a switch?> > switchErr(std::move(Err)) > .case< MyCustomError>([] () { /* … */ }) > .case< MyOtherCustomError>([] () { /* … */ }) > .default([] () { /* … */ })It's similar to a switch, but it's actually more like a list of regular C++ exception catch blocks (the name 'catchTypedError' is a nod to this). The big difference is that you're not trying to find "the matching handler" in the set of options. Instead, the list of handlers is evaluated in order until one is found that fits, then that handler alone is executed. So if you had the following: class MyBaseError : public TypedErrorInfo<MyBaseError> {}; class MyDerivedError : public TypedErrorInfo<MyDerivedError, MyBaseError> {}; // <- MyDerivedError inherits from MyBaseError. and you wrote something like this: catchTypedErrors(std::move(Err), handleTypedError<MyBaseError>([&](std::unique_ptr<MyBaseError> B) { }), handleTypedError<MyDerivedError>([&](std::unique_ptr<MyDerivedError> D) { }) ); The second handler will never run: All 'Derived' errors are 'Base' errors, the first handler fits, so it's the one that will be run. We could go for something more like a switch, but then you have to define the notion of "best fit" for a type, which might be difficult (especially if I extend this to support multiple inheritance in error hierarchies. ;). I think it's easier to reason about "first handler that fits". Cheers, Lang. On Tue, Feb 2, 2016 at 6:33 PM, Mehdi Amini <mehdi.amini at apple.com> wrote:> Hi Lang, > > I’m glad someone tackle this long lived issue :) > I’ve started to think about it recently but didn’t as far as you did! > > On Feb 2, 2016, at 5:29 PM, Lang Hames via llvm-dev < > llvm-dev at lists.llvm.org> wrote: > > Hi All, > > I've been thinking lately about how to improve LLVM's error model and > error reporting. A lack of good error reporting in Orc and MCJIT has forced > me to spend a lot of time investigating hard-to-debug errors that could > easily have been identified if we provided richer error information to the > client, rather than just aborting. Kevin Enderby has made similar > observations about the state of libObject and the difficulty of producing > good error messages for damaged object files. I expect to encounter more > issues like this as I continue work on the MachO side of LLD. I see > tackling the error modeling problem as a first step towards improving error > handling in general: if we make it easy to model errors, it may pave the > way for better error handling in many parts of our libraries. > > At present in LLVM we model errors with std::error_code (and its helper, > ErrorOr) and use diagnostic streams for error reporting. Neither of these > seem entirely up to the job of providing a solid error-handling mechanism > for library code. Diagnostic streams are great if all you want to do is > report failure to the user and then terminate, but they can't be used to > distinguish between different kinds of errors > > > I’m not sure to understand this claim? You are supposed to be able to > extend and subclass the type of diagnostics? (I remember doing it for an > out-of-tree LLVM-based project). > > > , and so are unsuited to many use-cases (especially error recovery). On > the other hand, std::error_code allows error kinds to be distinguished, but > suffers a number of drawbacks: > > 1. It carries no context: It tells you what went wrong, but not where or > why, making it difficult to produce good diagnostics. > 2. It's extremely easy to ignore or forget: instances can be silently > dropped. > 3. It's not especially debugger friendly: Most people call the error_code > constructors directly for both success and failure values. Breakpoints have > to be set carefully to avoid stopping when success values are constructed. > > In fairness to std::error_code, it has some nice properties too: > > 1. It's extremely lightweight. > 2. It's explicit in the API (unlike exceptions). > 3. It doesn't require C++ RTTI (a requirement for use in LLVM). > > To address these shortcomings I have prototyped a new error-handling > scheme partially inspired by C++ exceptions. The aim was to preserve the > performance and API visibility of std::error_code, while allowing users to > define custom error classes and inheritance relationships between them. My > hope is that library code could use this scheme to model errors in a > meaningful way, allowing clients to inspect the error information and > recover where possible, or provide a rich diagnostic when aborting. > > The scheme has three major "moving parts": > > 1. A new 'TypedError' class that can be used as a replacement for > std::error_code. E.g. > > std::error_code foo(); > > becomes > > TypedError foo(); > > The TypedError class serves as a lightweight wrapper for the real error > information (see (2)). It also contains a 'Checked' flag, initially set to > false, that tracks whether the error has been handled or not. If a > TypedError is ever destructed without being checked (or passed on to > someone else) it will call std::terminate(). TypedError cannot be silently > dropped. > > > I really like the fact that not checking the error triggers an error (this > is the "hard to misuse” part of API design IMO). > You don’t mention it, but I’d rather see this “checked” flag compiled out > with NDEBUG. > > > 2. A utility class, TypedErrorInfo, for building error class hierarchies > rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. > > // Define a new error type implicitly inheriting from TypedErrorInfoBase. > class MyCustomError : public TypedErrorInfo<MyCustomError> { > public: > // Custom error info. > }; > > // Define a subclass of MyCustomError. > class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, > MyCustomError> { > public: > // Extends MyCustomError, adds new members. > }; > > 3. A set of utility functions that use the custom RTTI system to inspect > and handle typed errors. For example 'catchAllTypedErrors' and > 'handleTypedError' cooperate to handle error instances in a type-safe way: > > TypedError foo() { > if (SomeFailureCondition) > return make_typed_error<MyCustomError>(); > } > > TypedError Err = foo(); > > catchAllTypedErrors(std::move(Err), > handleTypedError<MyCustomError>( > [](std::unique_ptr<MyCustomError> E) { > // Handle the error. > return TypedError(); // <- Indicate success from handler. > > > What does success or failure means for the handler? > > > } > ) > ); > > > If your initial reaction is "Too much boilerplate!" I understand, but take > comfort: (1) In the overwhelmingly common case of simply returning errors, > the usage is identical to std::error_code: > > if (TypedError Err = foo()) > return Err; > > and (2) the boilerplate for catching errors is usually easily contained in > a handful of utility functions, and tends not to crowd the rest of your > source code. My initial experiments with this scheme involved updating many > source lines, but did not add much code at all beyond the new error classes > that were introduced. > > > I believe that this scheme addresses many of the shortcomings of > std::error_code while maintaining the strengths: > > 1. Context - Custom error classes enable the user to attach as much > contextual information as desired. > > 2. Difficult to drop - The 'checked' flag in TypedError ensures that it > can't be dropped, it must be explicitly "handled", even if that only > involves catching the error and doing nothing. > > 3. Debugger friendly - You can set a breakpoint on any custom error > class's constructor to catch that error being created. Since the error > class hierarchy is rooted you can break on > TypedErrorInfoBase::TypedErrorInfoBase to catch any error being raised. > > 4. Lightweight - Because TypedError instances are just a pointer and a > checked-bit, move-constructing it is very cheap. We may also want to > consider ignoring the 'checked' bit in release mode, at which point > TypedError should be as cheap as std::error_code. > > > Oh here you mention compiling out the “checked” flag :) > > > 5. Explicit - TypedError is represented explicitly in the APIs, the same > as std::error_code. > > 6. Does not require C++ RTTI - The custom RTTI system does not rely on any > standard C++ RTTI features. > > This scheme also has one attribute that I haven't seen in previous error > handling systems (though my experience in this area is limited): Errors are > not copyable, due to ownership semantics of TypedError. I think this > actually neatly captures the idea that there is a chain of responsibility > for dealing with any given error. Responsibility may be transferred (e.g. > by returning it to a caller), but it cannot be duplicated as it doesn't > generally make sense for multiple people to report or attempt to recover > from the same error. > > I've tested this prototype out by threading it through the object-creation > APIs of libObject and using custom error classes to report errors in MachO > headers. My initial experience is that this has enabled much richer error > messages than are possible with std::error_code. > > To enable interaction with APIs that still use std::error_code I have > added a custom ECError class that wraps a std::error_code, and can be > converted back to a std::error_code using the typedErrorToErrorCode > function. For now, all custom error code classes should (and do, in the > prototype) derive from this utility class. In my experiments, this has made > it easy to thread TypedError selectively through parts of the API. > Eventually my hope is that TypedError could replace std::error_code for > user-facing APIs, at which point custom errors would no longer need to > derive from ECError, and ECError could be relegated to a utility for > interacting with other codebases that still use std::error_code. > > So - I look forward to hearing your thoughts. :) > > > Is your call to catchAllTypedErrors(…) actually like a switch on the type > of the error? What about a syntax that looks like a switch? > > switchErr(std::move(Err)) > .case< MyCustomError>([] () { /* … */ }) > .case< MyOtherCustomError>([] () { /* … */ }) > .default([] () { /* … */ }) > > > — > Mehdi > > > Cheers, > Lang. > > Attached files: > > typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds > anchor() method to lib/Support/ErrorHandling.cpp). > > error_demo.tgz - Stand-alone program demo'ing basic use of the TypedError > API. > > libobject_typed_error_demo.patch - Threads TypedError through the > binary-file creation methods (createBinary, createObjectFile, etc). > Proof-of-concept for how TypedError can be integrated into an existing > system. > > <typed_error.patch><error_demo.tgz> > <thread_typederror_through_object_creation.patch> > _______________________________________________ > LLVM Developers mailing list > llvm-dev at lists.llvm.org > http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev > > >-------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160202/ae910786/attachment.html>
Craig, Ben via llvm-dev
2016-Feb-03 14:15 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
I've had some experience dealing with rich error descriptions without exceptions before. The scheme I used was somewhat similar to what you have. Here are some items to consider. * How will the following code be avoided? The answer may be compile time error, runtime error, style recommendations, or maybe something else. TypedError Err = foo(); // no checking in between Err = foo(); * How about this? TypedError Err = foo(); functionWithHorribleSideEffects(); if(Err) return; * Do you anticipate giving these kinds of errors to out of tree projects? If so, are there any kind of binary compatibility guarantee? * What about errors that should come out of constructors? Or <shudder> destructors? * If a constructor fails and doesn't establish it's invariant, what will prevent the use of that invalid object? * How many subclasses do you expect to make of TypedError? Less than 10? More than 100? * How common is it to want to handle a specific error code in a non-local way? In my experience, I either want a specific error handled locally, or a fail / not-failed from farther away. The answer to this question may influence the number of subclasses you want to make. * Are file, line number, and / or call stack information captured? I've found file and line number information to be incredibly useful from a productivity standpoint. On 2/2/2016 7:29 PM, Lang Hames via llvm-dev wrote:> Hi All, > > I've been thinking lately about how to improve LLVM's error model and > error reporting. A lack of good error reporting in Orc and MCJIT has > forced me to spend a lot of time investigating hard-to-debug errors > that could easily have been identified if we provided richer error > information to the client, rather than just aborting. Kevin Enderby > has made similar observations about the state of libObject and the > difficulty of producing good error messages for damaged object files. > I expect to encounter more issues like this as I continue work on the > MachO side of LLD. I see tackling the error modeling problem as a > first step towards improving error handling in general: if we make it > easy to model errors, it may pave the way for better error handling in > many parts of our libraries. > > At present in LLVM we model errors with std::error_code (and its > helper, ErrorOr) and use diagnostic streams for error reporting. > Neither of these seem entirely up to the job of providing a solid > error-handling mechanism for library code. Diagnostic streams are > great if all you want to do is report failure to the user and then > terminate, but they can't be used to distinguish between different > kinds of errors, and so are unsuited to many use-cases (especially > error recovery). On the other hand, std::error_code allows error kinds > to be distinguished, but suffers a number of drawbacks: > > 1. It carries no context: It tells you what went wrong, but not where > or why, making it difficult to produce good diagnostics. > 2. It's extremely easy to ignore or forget: instances can be silently > dropped. > 3. It's not especially debugger friendly: Most people call the > error_code constructors directly for both success and failure values. > Breakpoints have to be set carefully to avoid stopping when success > values are constructed. > > In fairness to std::error_code, it has some nice properties too: > > 1. It's extremely lightweight. > 2. It's explicit in the API (unlike exceptions). > 3. It doesn't require C++ RTTI (a requirement for use in LLVM). > > To address these shortcomings I have prototyped a new error-handling > scheme partially inspired by C++ exceptions. The aim was to preserve > the performance and API visibility of std::error_code, while allowing > users to define custom error classes and inheritance relationships > between them. My hope is that library code could use this scheme to > model errors in a meaningful way, allowing clients to inspect the > error information and recover where possible, or provide a rich > diagnostic when aborting. > > The scheme has three major "moving parts": > > 1. A new 'TypedError' class that can be used as a replacement for > std::error_code. E.g. > > std::error_code foo(); > > becomes > > TypedError foo(); > > The TypedError class serves as a lightweight wrapper for the real > error information (see (2)). It also contains a 'Checked' flag, > initially set to false, that tracks whether the error has been handled > or not. If a TypedError is ever destructed without being checked (or > passed on to someone else) it will call std::terminate(). TypedError > cannot be silently dropped. > > 2. A utility class, TypedErrorInfo, for building error class > hierarchies rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. > > // Define a new error type implicitly inheriting from TypedErrorInfoBase. > class MyCustomError : public TypedErrorInfo<MyCustomError> { > public: > // Custom error info. > }; > > // Define a subclass of MyCustomError. > class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, > MyCustomError> { > public: > // Extends MyCustomError, adds new members. > }; > > 3. A set of utility functions that use the custom RTTI system to > inspect and handle typed errors. For example 'catchAllTypedErrors' and > 'handleTypedError' cooperate to handle error instances in a type-safe way: > > TypedError foo() { > if (SomeFailureCondition) > return make_typed_error<MyCustomError>(); > } > > TypedError Err = foo(); > > catchAllTypedErrors(std::move(Err), > handleTypedError<MyCustomError>( > [](std::unique_ptr<MyCustomError> E) { > // Handle the error. > return TypedError(); // <- Indicate success from handler. > } > ) > ); > > > If your initial reaction is "Too much boilerplate!" I understand, but > take comfort: (1) In the overwhelmingly common case of simply > returning errors, the usage is identical to std::error_code: > > if (TypedError Err = foo()) > return Err; > > and (2) the boilerplate for catching errors is usually easily > contained in a handful of utility functions, and tends not to crowd > the rest of your source code. My initial experiments with this scheme > involved updating many source lines, but did not add much code at all > beyond the new error classes that were introduced. > > > I believe that this scheme addresses many of the shortcomings of > std::error_code while maintaining the strengths: > > 1. Context - Custom error classes enable the user to attach as much > contextual information as desired. > > 2. Difficult to drop - The 'checked' flag in TypedError ensures that > it can't be dropped, it must be explicitly "handled", even if that > only involves catching the error and doing nothing. > > 3. Debugger friendly - You can set a breakpoint on any custom error > class's constructor to catch that error being created. Since the error > class hierarchy is rooted you can break on > TypedErrorInfoBase::TypedErrorInfoBase to catch any error being raised. > > 4. Lightweight - Because TypedError instances are just a pointer and a > checked-bit, move-constructing it is very cheap. We may also want to > consider ignoring the 'checked' bit in release mode, at which point > TypedError should be as cheap as std::error_code. > > 5. Explicit - TypedError is represented explicitly in the APIs, the > same as std::error_code. > > 6. Does not require C++ RTTI - The custom RTTI system does not rely on > any standard C++ RTTI features. > > This scheme also has one attribute that I haven't seen in previous > error handling systems (though my experience in this area is limited): > Errors are not copyable, due to ownership semantics of TypedError. I > think this actually neatly captures the idea that there is a chain of > responsibility for dealing with any given error. Responsibility may be > transferred (e.g. by returning it to a caller), but it cannot be > duplicated as it doesn't generally make sense for multiple people to > report or attempt to recover from the same error. > > I've tested this prototype out by threading it through the > object-creation APIs of libObject and using custom error classes to > report errors in MachO headers. My initial experience is that this has > enabled much richer error messages than are possible with std::error_code. > > To enable interaction with APIs that still use std::error_code I have > added a custom ECError class that wraps a std::error_code, and can be > converted back to a std::error_code using the typedErrorToErrorCode > function. For now, all custom error code classes should (and do, in > the prototype) derive from this utility class. In my experiments, this > has made it easy to thread TypedError selectively through parts of the > API. Eventually my hope is that TypedError could replace > std::error_code for user-facing APIs, at which point custom errors > would no longer need to derive from ECError, and ECError could be > relegated to a utility for interacting with other codebases that still > use std::error_code. > > So - I look forward to hearing your thoughts. :) > > Cheers, > Lang. > > Attached files: > > typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds > anchor() method to lib/Support/ErrorHandling.cpp). > > error_demo.tgz - Stand-alone program demo'ing basic use of the > TypedError API. > > libobject_typed_error_demo.patch - Threads TypedError through the > binary-file creation methods (createBinary, createObjectFile, etc). > Proof-of-concept for how TypedError can be integrated into an existing > system. > > > > _______________________________________________ > LLVM Developers mailing list > llvm-dev at lists.llvm.org > http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev-- Employee of Qualcomm Innovation Center, Inc. Qualcomm Innovation Center, Inc. is a member of Code Aurora Forum, a Linux Foundation Collaborative Project -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160203/a2cb31e5/attachment.html>
Vedant Kumar via llvm-dev
2016-Feb-03 18:02 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Hi Lang,> I expect to encounter more issues like this as I continue work on the MachO side of LLD. I see tackling the error modeling problem as a first step towards improving error handling in general: if we make it easy to model errors, it may pave the way for better error handling in many parts of our libraries.+ 1, I'd like to use this throughout lib/ProfileData. It's a bit frustrating to see crashers which simply state: "Malformed profile data". It'd great to know _where_ the issue actually is. vedant
Lang Hames via llvm-dev
2016-Feb-03 18:18 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Hi Craig,> TypedError Err = foo(); > // no checking in between > Err = foo();This will cause an abort - the assignment operator for TypedError checks that you're not overwriting an unhanded error.> TypedError Err = foo(); > functionWithHorribleSideEffects(); > if (Err) return;This is potentially reasonable code - it's impossible to distinguish in general from: TypedError Err = foo(); functionWithPerfectlyReasonableSideEffects(); if (Err) return; That said, to avoid problems related to this style we can offer style guidelines. Idiomatic usage of the system looks like: if (auto Err = foo()) return Err; functionWithHorribleSideEffects(); This is how people tend to write error checks in most of the LLVM code I've seen to date.> Do you anticipate giving these kinds of errors to out of tree projects?If so, are there any kind of binary compatibility guarantee? Out of tree projects can use the TypedError.h header and derive their own error classes. This is all pure C++, I don't think there are binary compatibility issues.> What about errors that should come out of constructors? Or <shudder>destructors? TypedError can't be "thrown" in the same way that C++ exceptions can. It's an ordinary C++ value. You can't return an error from a constructor, but you can pass a reference to an error in and set that. In general the style guideline for a "may-fail" constructors would be to write something like this: class Foo { public: static TypedErrorOr<Foo> create(int X, int Y) { TypedError Err; Foo F(X, Y, Err); if (Err) return std::move(Err); return std::move(F); } private: Foo(int x, int y, TypedError &Err) { if (x == y) { Err = make_typed_error<BadFoo>(); return; } } }; Then you have: TypedErrorOr<Foo> F = Foo::create(X, Y); The only way to catch failure of a destructor is for the class to hold a reference to a TypedError, and set that. This is extremely difficult to do correctly, but as far is I know all error schemes suffer from poor interaction with destructors. In LLVM failing destructors are very rare, so I don't anticipate this being a problem in general.> If a constructor fails and doesn't establish it's invariant, what willprevent the use of that invalid object? If the style guideline above is followed the invalid object will never be returned to the user. Care must be taken to ensure that the destructor can destruct the partially constructed object, but that's always the case.> How many subclasses do you expect to make of TypedError? Less than 10?More than 100? This is a support library, so it's not possible to reason about how many external clients will want to use it in their projects, or how many errors they would define. In LLVM I'd like to see us adopt a 'less-is-more' approach: New error types should be introduced sparingly, and each new error type should require a rationale for its existence. In particular, distinct error types should only be introduced when it's reasonable for some client to make a meaningful distinction between them. If an error is only being returned in order to produce a string diagnostic, a generic StringDiagnosticError should suffice. Answering your question more directly: In the LLVM code I'm familiar with I can see room for more than 10 error types, but fewer than 100.> How common is it to want to handle a specific error code in a non-localway? In my experience, I either want a specific error handled locally, or a fail / not-failed from farther away. The answer to this question may influence the number of subclasses you want to make. Errors usually get handled locally, or just produce a diagnostic and failure, however there are some cases where we want non-local recovery from specific errors. The archive-walking example I gave earlier is one such case. You're right on the point about subclasses too - that's what I was hoping to capture with my comment above: only introduce an error type if it's meaningful for a client to distinguish it from other errors. > Are file, line number, and / or call stack information captured? I've found file and line number information to be incredibly useful from a productivity standpoint. I think that information is helpful for programmatic errors, but those are better represented by asserts or "report_fatal_error". This system is intended to support modelling of non-programmatic errors - bad input, resource failures and the like. For those, the specific point in the code where the error was triggered is less useful. If such information is needed, this system makes it easy to break on the failure point in a debugger. Cheers, Lang. On Wed, Feb 3, 2016 at 6:15 AM, Craig, Ben via llvm-dev < llvm-dev at lists.llvm.org> wrote:> I've had some experience dealing with rich error descriptions without > exceptions before. The scheme I used was somewhat similar to what you > have. Here are some items to consider. > > * How will the following code be avoided? The answer may be compile time > error, runtime error, style recommendations, or maybe something else. > > TypedError Err = foo(); > // no checking in between > Err = foo(); > > * How about this? > > TypedError Err = foo(); > functionWithHorribleSideEffects(); > if(Err) return; > > * Do you anticipate giving these kinds of errors to out of tree projects? > If so, are there any kind of binary compatibility guarantee? > > * What about errors that should come out of constructors? Or <shudder> > destructors? > > * If a constructor fails and doesn't establish it's invariant, what will > prevent the use of that invalid object? > > * How many subclasses do you expect to make of TypedError? Less than 10? > More than 100? > > * How common is it to want to handle a specific error code in a non-local > way? In my experience, I either want a specific error handled locally, or > a fail / not-failed from farther away. The answer to this question may > influence the number of subclasses you want to make. > > * Are file, line number, and / or call stack information captured? I've > found file and line number information to be incredibly useful from a > productivity standpoint. > > > On 2/2/2016 7:29 PM, Lang Hames via llvm-dev wrote: > > Hi All, > > I've been thinking lately about how to improve LLVM's error model and > error reporting. A lack of good error reporting in Orc and MCJIT has forced > me to spend a lot of time investigating hard-to-debug errors that could > easily have been identified if we provided richer error information to the > client, rather than just aborting. Kevin Enderby has made similar > observations about the state of libObject and the difficulty of producing > good error messages for damaged object files. I expect to encounter more > issues like this as I continue work on the MachO side of LLD. I see > tackling the error modeling problem as a first step towards improving error > handling in general: if we make it easy to model errors, it may pave the > way for better error handling in many parts of our libraries. > > At present in LLVM we model errors with std::error_code (and its helper, > ErrorOr) and use diagnostic streams for error reporting. Neither of these > seem entirely up to the job of providing a solid error-handling mechanism > for library code. Diagnostic streams are great if all you want to do is > report failure to the user and then terminate, but they can't be used to > distinguish between different kinds of errors, and so are unsuited to many > use-cases (especially error recovery). On the other hand, std::error_code > allows error kinds to be distinguished, but suffers a number of drawbacks: > > 1. It carries no context: It tells you what went wrong, but not where or > why, making it difficult to produce good diagnostics. > 2. It's extremely easy to ignore or forget: instances can be silently > dropped. > 3. It's not especially debugger friendly: Most people call the error_code > constructors directly for both success and failure values. Breakpoints have > to be set carefully to avoid stopping when success values are constructed. > > In fairness to std::error_code, it has some nice properties too: > > 1. It's extremely lightweight. > 2. It's explicit in the API (unlike exceptions). > 3. It doesn't require C++ RTTI (a requirement for use in LLVM). > > To address these shortcomings I have prototyped a new error-handling > scheme partially inspired by C++ exceptions. The aim was to preserve the > performance and API visibility of std::error_code, while allowing users to > define custom error classes and inheritance relationships between them. My > hope is that library code could use this scheme to model errors in a > meaningful way, allowing clients to inspect the error information and > recover where possible, or provide a rich diagnostic when aborting. > > The scheme has three major "moving parts": > > 1. A new 'TypedError' class that can be used as a replacement for > std::error_code. E.g. > > std::error_code foo(); > > becomes > > TypedError foo(); > > The TypedError class serves as a lightweight wrapper for the real error > information (see (2)). It also contains a 'Checked' flag, initially set to > false, that tracks whether the error has been handled or not. If a > TypedError is ever destructed without being checked (or passed on to > someone else) it will call std::terminate(). TypedError cannot be silently > dropped. > > 2. A utility class, TypedErrorInfo, for building error class hierarchies > rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. > > // Define a new error type implicitly inheriting from TypedErrorInfoBase. > class MyCustomError : public TypedErrorInfo<MyCustomError> { > public: > // Custom error info. > }; > > // Define a subclass of MyCustomError. > class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, > MyCustomError> { > public: > // Extends MyCustomError, adds new members. > }; > > 3. A set of utility functions that use the custom RTTI system to inspect > and handle typed errors. For example 'catchAllTypedErrors' and > 'handleTypedError' cooperate to handle error instances in a type-safe way: > > TypedError foo() { > if (SomeFailureCondition) > return make_typed_error<MyCustomError>(); > } > > TypedError Err = foo(); > > catchAllTypedErrors(std::move(Err), > handleTypedError<MyCustomError>( > [](std::unique_ptr<MyCustomError> E) { > // Handle the error. > return TypedError(); // <- Indicate success from handler. > } > ) > ); > > > If your initial reaction is "Too much boilerplate!" I understand, but take > comfort: (1) In the overwhelmingly common case of simply returning errors, > the usage is identical to std::error_code: > > if (TypedError Err = foo()) > return Err; > > and (2) the boilerplate for catching errors is usually easily contained in > a handful of utility functions, and tends not to crowd the rest of your > source code. My initial experiments with this scheme involved updating many > source lines, but did not add much code at all beyond the new error classes > that were introduced. > > > I believe that this scheme addresses many of the shortcomings of > std::error_code while maintaining the strengths: > > 1. Context - Custom error classes enable the user to attach as much > contextual information as desired. > > 2. Difficult to drop - The 'checked' flag in TypedError ensures that it > can't be dropped, it must be explicitly "handled", even if that only > involves catching the error and doing nothing. > > 3. Debugger friendly - You can set a breakpoint on any custom error > class's constructor to catch that error being created. Since the error > class hierarchy is rooted you can break on > TypedErrorInfoBase::TypedErrorInfoBase to catch any error being raised. > > 4. Lightweight - Because TypedError instances are just a pointer and a > checked-bit, move-constructing it is very cheap. We may also want to > consider ignoring the 'checked' bit in release mode, at which point > TypedError should be as cheap as std::error_code. > > 5. Explicit - TypedError is represented explicitly in the APIs, the same > as std::error_code. > > 6. Does not require C++ RTTI - The custom RTTI system does not rely on any > standard C++ RTTI features. > > This scheme also has one attribute that I haven't seen in previous error > handling systems (though my experience in this area is limited): Errors are > not copyable, due to ownership semantics of TypedError. I think this > actually neatly captures the idea that there is a chain of responsibility > for dealing with any given error. Responsibility may be transferred (e.g. > by returning it to a caller), but it cannot be duplicated as it doesn't > generally make sense for multiple people to report or attempt to recover > from the same error. > > I've tested this prototype out by threading it through the object-creation > APIs of libObject and using custom error classes to report errors in MachO > headers. My initial experience is that this has enabled much richer error > messages than are possible with std::error_code. > > To enable interaction with APIs that still use std::error_code I have > added a custom ECError class that wraps a std::error_code, and can be > converted back to a std::error_code using the typedErrorToErrorCode > function. For now, all custom error code classes should (and do, in the > prototype) derive from this utility class. In my experiments, this has made > it easy to thread TypedError selectively through parts of the API. > Eventually my hope is that TypedError could replace std::error_code for > user-facing APIs, at which point custom errors would no longer need to > derive from ECError, and ECError could be relegated to a utility for > interacting with other codebases that still use std::error_code. > > So - I look forward to hearing your thoughts. :) > > Cheers, > Lang. > > Attached files: > > typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds > anchor() method to lib/Support/ErrorHandling.cpp). > > error_demo.tgz - Stand-alone program demo'ing basic use of the TypedError > API. > > libobject_typed_error_demo.patch - Threads TypedError through the > binary-file creation methods (createBinary, createObjectFile, etc). > Proof-of-concept for how TypedError can be integrated into an existing > system. > > > > _______________________________________________ > LLVM Developers mailing listllvm-dev at lists.llvm.orghttp://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev > > > -- > Employee of Qualcomm Innovation Center, Inc. > Qualcomm Innovation Center, Inc. is a member of Code Aurora Forum, a Linux Foundation Collaborative Project > > > _______________________________________________ > LLVM Developers mailing list > llvm-dev at lists.llvm.org > http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev > >-------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160203/516f6ea0/attachment-0001.html>
Philip Reames via llvm-dev
2016-Feb-03 23:02 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
On 02/02/2016 05:29 PM, Lang Hames via llvm-dev wrote:> Hi All, > > I've been thinking lately about how to improve LLVM's error model and > error reporting. A lack of good error reporting in Orc and MCJIT has > forced me to spend a lot of time investigating hard-to-debug errors > that could easily have been identified if we provided richer error > information to the client, rather than just aborting. Kevin Enderby > has made similar observations about the state of libObject and the > difficulty of producing good error messages for damaged object files. > I expect to encounter more issues like this as I continue work on the > MachO side of LLD. I see tackling the error modeling problem as a > first step towards improving error handling in general: if we make it > easy to model errors, it may pave the way for better error handling in > many parts of our libraries. >Separate from the discussion on how we represent errors, we need to decide what errors we want to detect, which we try to diagnose, and which are recoverable. Each of these is a distinct design decision. For instance, I'd be really curious to hear more about your use case in Orc and MCJIT. We're using MCJIT and I've found the error model presented to be perfectly adequate for our purposes. I've sometimes wished that MCJIT did a slightly better job *diagnosing* failures, but I have yet to see a case where I'd actually want the compiler to recover from incorrect input rather than failing with a clean message so that I can fix the bug in the calling code. One thing I'm seriously concerned about is setting false expectations. Unless we are actually going to invest in supporting recovery from (say) malformed input, we should not present an interface which seems to allow that possibility. A hard failure is *much* preferable to a soft error with the library in a potentially corrupt state. I have seen several software projects head down the road of soft errors with partial recovery and it's *always* a disaster. (Your proposal around checked-by-default errors is a good step in this direction, but is not by itself enough.) Philip p.s. I'm purposely not commenting on the representation question because I haven't read the proposal in enough detail to have a strong opinion. -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160203/270805c2/attachment.html>
Lang Hames via llvm-dev
2016-Feb-03 23:19 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
Hi Philip,> Separate from the discussion on how we represent errors, we need todecide what errors we want to detect, which we try to diagnose, and which are recoverable. Each of these is a distinct design decision. Absolutely. This proposal should be interpreted with that in mind - it gives us a better toolkit for describing certain kinds of errors, but it should not be shoehorned into situations where it does not fit. Programmatic errors are an obvious example: they should remain hard errors (asserts/report_fatal_error/llvm_unreachable). On MCJIT/Orc - I'm glad the current situation hasn't caused you too much pain, but other clients have complained about it. I think the key observation is that neither MCJIT nor ORC assume that the bit code they're compiling is in-memory. That means that both should be able to report "resource not available" errors (e.g. when JITing bitcode off a network mount that has temporarily dropped out). This problem has become especially acute now that we have library support for remote-JITing: Any call to any lazily compiled function may fail due to resource drop-outs, and we should be able to report that to the user to give them a chance to try to recover and re-try compilation. On the commitment front: The driving use case for me currently is the llvm object-file tools and LLD, but I know that proper error handling in the JIT is something I want to tackle in not-too-distant future, so having a toolkit to deal with it is a big step in the right direction. - Lang. On Wed, Feb 3, 2016 at 3:02 PM, Philip Reames <listmail at philipreames.com> wrote:> > > On 02/02/2016 05:29 PM, Lang Hames via llvm-dev wrote: > > Hi All, > > I've been thinking lately about how to improve LLVM's error model and > error reporting. A lack of good error reporting in Orc and MCJIT has forced > me to spend a lot of time investigating hard-to-debug errors that could > easily have been identified if we provided richer error information to the > client, rather than just aborting. Kevin Enderby has made similar > observations about the state of libObject and the difficulty of producing > good error messages for damaged object files. I expect to encounter more > issues like this as I continue work on the MachO side of LLD. I see > tackling the error modeling problem as a first step towards improving error > handling in general: if we make it easy to model errors, it may pave the > way for better error handling in many parts of our libraries. > > Separate from the discussion on how we represent errors, we need to decide > what errors we want to detect, which we try to diagnose, and which are > recoverable. Each of these is a distinct design decision. > > For instance, I'd be really curious to hear more about your use case in > Orc and MCJIT. We're using MCJIT and I've found the error model presented > to be perfectly adequate for our purposes. I've sometimes wished that > MCJIT did a slightly better job *diagnosing* failures, but I have yet to > see a case where I'd actually want the compiler to recover from incorrect > input rather than failing with a clean message so that I can fix the bug in > the calling code. > > One thing I'm seriously concerned about is setting false expectations. > Unless we are actually going to invest in supporting recovery from (say) > malformed input, we should not present an interface which seems to allow > that possibility. A hard failure is *much* preferable to a soft error with > the library in a potentially corrupt state. I have seen several software > projects head down the road of soft errors with partial recovery and it's > *always* a disaster. (Your proposal around checked-by-default errors is a > good step in this direction, but is not by itself enough.) > > Philip > > p.s. I'm purposely not commenting on the representation question because I > haven't read the proposal in enough detail to have a strong opinion. > > > >-------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20160203/83886869/attachment.html>
Michael Spencer via llvm-dev
2016-Feb-18 01:10 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
On Tue, Feb 2, 2016 at 5:29 PM, Lang Hames via llvm-dev <llvm-dev at lists.llvm.org> wrote:> Hi All, > > I've been thinking lately about how to improve LLVM's error model and error > reporting. A lack of good error reporting in Orc and MCJIT has forced me to > spend a lot of time investigating hard-to-debug errors that could easily > have been identified if we provided richer error information to the client, > rather than just aborting. Kevin Enderby has made similar observations about > the state of libObject and the difficulty of producing good error messages > for damaged object files. I expect to encounter more issues like this as I > continue work on the MachO side of LLD. I see tackling the error modeling > problem as a first step towards improving error handling in general: if we > make it easy to model errors, it may pave the way for better error handling > in many parts of our libraries. > > At present in LLVM we model errors with std::error_code (and its helper, > ErrorOr) and use diagnostic streams for error reporting. Neither of these > seem entirely up to the job of providing a solid error-handling mechanism > for library code. Diagnostic streams are great if all you want to do is > report failure to the user and then terminate, but they can't be used to > distinguish between different kinds of errors, and so are unsuited to many > use-cases (especially error recovery). On the other hand, std::error_code > allows error kinds to be distinguished, but suffers a number of drawbacks: > > 1. It carries no context: It tells you what went wrong, but not where or > why, making it difficult to produce good diagnostics. > 2. It's extremely easy to ignore or forget: instances can be silently > dropped. > 3. It's not especially debugger friendly: Most people call the error_code > constructors directly for both success and failure values. Breakpoints have > to be set carefully to avoid stopping when success values are constructed. > > In fairness to std::error_code, it has some nice properties too: > > 1. It's extremely lightweight. > 2. It's explicit in the API (unlike exceptions). > 3. It doesn't require C++ RTTI (a requirement for use in LLVM). > > To address these shortcomings I have prototyped a new error-handling scheme > partially inspired by C++ exceptions. The aim was to preserve the > performance and API visibility of std::error_code, while allowing users to > define custom error classes and inheritance relationships between them. My > hope is that library code could use this scheme to model errors in a > meaningful way, allowing clients to inspect the error information and > recover where possible, or provide a rich diagnostic when aborting. > > The scheme has three major "moving parts": > > 1. A new 'TypedError' class that can be used as a replacement for > std::error_code. E.g. > > std::error_code foo(); > > becomes > > TypedError foo(); > > The TypedError class serves as a lightweight wrapper for the real error > information (see (2)). It also contains a 'Checked' flag, initially set to > false, that tracks whether the error has been handled or not. If a > TypedError is ever destructed without being checked (or passed on to someone > else) it will call std::terminate(). TypedError cannot be silently dropped. > > 2. A utility class, TypedErrorInfo, for building error class hierarchies > rooted at 'TypedErrorInfoBase' with custom RTTI. E.g. > > // Define a new error type implicitly inheriting from TypedErrorInfoBase. > class MyCustomError : public TypedErrorInfo<MyCustomError> { > public: > // Custom error info. > }; > > // Define a subclass of MyCustomError. > class MyCustomSubError : public TypedErrorInfo<MyCustomSubError, > MyCustomError> { > public: > // Extends MyCustomError, adds new members. > }; > > 3. A set of utility functions that use the custom RTTI system to inspect > and handle typed errors. For example 'catchAllTypedErrors' and > 'handleTypedError' cooperate to handle error instances in a type-safe way: > > TypedError foo() { > if (SomeFailureCondition) > return make_typed_error<MyCustomError>(); > } > > TypedError Err = foo(); > > catchAllTypedErrors(std::move(Err), > handleTypedError<MyCustomError>( > [](std::unique_ptr<MyCustomError> E) { > // Handle the error. > return TypedError(); // <- Indicate success from handler. > } > ) > ); > > > If your initial reaction is "Too much boilerplate!" I understand, but take > comfort: (1) In the overwhelmingly common case of simply returning errors, > the usage is identical to std::error_code: > > if (TypedError Err = foo()) > return Err; > > and (2) the boilerplate for catching errors is usually easily contained in a > handful of utility functions, and tends not to crowd the rest of your source > code. My initial experiments with this scheme involved updating many source > lines, but did not add much code at all beyond the new error classes that > were introduced. > > > I believe that this scheme addresses many of the shortcomings of > std::error_code while maintaining the strengths: > > 1. Context - Custom error classes enable the user to attach as much > contextual information as desired. > > 2. Difficult to drop - The 'checked' flag in TypedError ensures that it > can't be dropped, it must be explicitly "handled", even if that only > involves catching the error and doing nothing. > > 3. Debugger friendly - You can set a breakpoint on any custom error class's > constructor to catch that error being created. Since the error class > hierarchy is rooted you can break on TypedErrorInfoBase::TypedErrorInfoBase > to catch any error being raised. > > 4. Lightweight - Because TypedError instances are just a pointer and a > checked-bit, move-constructing it is very cheap. We may also want to > consider ignoring the 'checked' bit in release mode, at which point > TypedError should be as cheap as std::error_code. > > 5. Explicit - TypedError is represented explicitly in the APIs, the same as > std::error_code. > > 6. Does not require C++ RTTI - The custom RTTI system does not rely on any > standard C++ RTTI features. > > This scheme also has one attribute that I haven't seen in previous error > handling systems (though my experience in this area is limited): Errors are > not copyable, due to ownership semantics of TypedError. I think this > actually neatly captures the idea that there is a chain of responsibility > for dealing with any given error. Responsibility may be transferred (e.g. by > returning it to a caller), but it cannot be duplicated as it doesn't > generally make sense for multiple people to report or attempt to recover > from the same error. > > I've tested this prototype out by threading it through the object-creation > APIs of libObject and using custom error classes to report errors in MachO > headers. My initial experience is that this has enabled much richer error > messages than are possible with std::error_code. > > To enable interaction with APIs that still use std::error_code I have added > a custom ECError class that wraps a std::error_code, and can be converted > back to a std::error_code using the typedErrorToErrorCode function. For now, > all custom error code classes should (and do, in the prototype) derive from > this utility class. In my experiments, this has made it easy to thread > TypedError selectively through parts of the API. Eventually my hope is that > TypedError could replace std::error_code for user-facing APIs, at which > point custom errors would no longer need to derive from ECError, and ECError > could be relegated to a utility for interacting with other codebases that > still use std::error_code. > > So - I look forward to hearing your thoughts. :) > > Cheers, > Lang. > > Attached files: > > typed_error.patch - Adds include/llvm/Support/TypedError.h (also adds > anchor() method to lib/Support/ErrorHandling.cpp). > > error_demo.tgz - Stand-alone program demo'ing basic use of the TypedError > API. > > libobject_typed_error_demo.patch - Threads TypedError through the > binary-file creation methods (createBinary, createObjectFile, etc). > Proof-of-concept for how TypedError can be integrated into an existing > system. > > > _______________________________________________ > LLVM Developers mailing list > llvm-dev at lists.llvm.org > http://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev >I like this idea in general. It's a better implementation of what ErrorOr originally was before we removed the custom error support because it wasn't used. In fact I would actually support outright replacing ErrorOr with this if it can be done safely, as I find the name TypedErrorOr a bit long. - Michael Spencer
Rafael Espíndola via llvm-dev
2016-Feb-18 15:36 UTC
[llvm-dev] [RFC] Error handling in LLVM libraries.
> I like this idea in general. It's a better implementation of what > ErrorOr originally was before we removed the custom error support > because it wasn't used. In fact I would actually support outright > replacing ErrorOr with this if it can be done safely, as I find the > name TypedErrorOr a bit long.The main differences are * This will hopefully be used. * TypedErrorOr is really a TypedError or T. The situation before was not that ErrorOr was std::error_code or T. Since we are adding it, I would also support replacing every use of ErrorOr with TypedErrorOr and renaming it. Cheers, Rafael