Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/Guide/ERRORS.md
diff options
context:
space:
mode:
authorLeitwolf112026-10-18 20:00:39 +0300
committerLeitwolf112026-10-18 20:00:39 +0300
commit8d1fb4807edc7920fbecd0554c9c61625827e19c (patch)
treedd3d7e1d4f48ca27140a2bd5f970817ad02ae35a /Guide/ERRORS.md
downloadprogmasoft-cpp-8d1fb4807edc7920fbecd0554c9c61625827e19c.tar.gz
Write the first revision of Progmasoft C++ Style
Progmasoft C++ Style is a style guide for C++20, written for anyone who wants a complete rule set to adopt. It writes down the conventions the Progmasoft C++ projects already follow (naming, the clang-format and clang-tidy profiles, exception-free error handling, Doxygen) and decides what they left open. Thirteen chapters under Guide/: 1. Principles: what the guide optimizes for and how to decide a case it does not cover. 2. Formatting: the full .clang-format profile, the reason for each choice, and the rules the tool cannot apply. 3. Naming: casing for every kind of name, acronyms, choosing words, namespaces, files. 4. Files and headers: extensions, #pragma once, includes, what goes in a header, the order inside a class, file size, vendored code. 5. Language: functions, auto, initialization, const and constexpr, casts, integers, strings, enumerations, templates and concepts, lambdas, control flow, and the features not used. 6. Types and classes: struct or class, final, construction, the rule of zero, inheritance, operators, strong types, optional and variant. 7. Errors: no exceptions by default, Expected and Error, invariants, fatal defects, destructors, diagnostics. 8. Ownership and memory: owners and borrowers, views and lifetimes, RAII, containers and deterministic iteration, allocation, undefined behavior. 9. Comments and documentation: Doxygen for public declarations, comments that explain why, TODOs that name an issue. 10. C interfaces: C11 headers, prefixes, opaque types, ownership and failure across the boundary, stability. 11. Concurrency: sharing less, threads, locks, thread-safety annotations, atomics, statics. 12. Testing: unit tests, fuzzing, sanitizers, benchmarks, coverage. 13. Tooling: the compiler flags, the full .clang-tidy profile with the reason for every disabled check, other checks, version pinning. The guide prescribes no test, fuzzing or benchmark framework and no error library; it prescribes how they are used. ADOPTING.md explains how a project points to the guide, copies the profiles and records its deviations. CONTRIBUTING.md explains how to propose a change. The text is licensed under CC BY 4.0. The configuration and code examples are dedicated to the public domain under CC0 1.0, so projects can copy them under their own license. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'Guide/ERRORS.md')
-rw-r--r--Guide/ERRORS.md123
1 files changed, 123 insertions, 0 deletions
diff --git a/Guide/ERRORS.md b/Guide/ERRORS.md
new file mode 100644
index 0000000..e57c790
--- /dev/null
+++ b/Guide/ERRORS.md
@@ -0,0 +1,123 @@
+<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> -->
+<!-- SPDX-License-Identifier: CC-BY-4.0 -->
+
+# 7. Errors
+
+A failure is one of three things, and each has one way of being handled.
+
+| Kind | Example | Handled by |
+| --- | --- | --- |
+| **Recoverable failure** | a malformed input file, a missing path, a value out of range | returning `Expected<T>` or `Error` |
+| **Broken invariant** | a verifier that accepted an IR it must reject, an index the caller guaranteed in range | an assertion, or a fatal error in release builds |
+| **Unrecoverable environment** | out of memory, a corrupted heap | termination |
+
+## 7.1 No exceptions in libraries and tools
+
+Code is compiled with `-fno-exceptions` (`/clang:-fno-exceptions` with `clang-cl`). There is no `throw`, `try` or
+`catch` in first-party code, including tests, fuzzers and benchmarks.
+
+The reasons are practical: an exception is invisible in a signature, so a caller cannot see that a call can fail; it
+cannot cross a C interface; and its cost and its interaction with destructors are hard to reason about in code that also
+runs under sanitizers and fuzzers.
+
+**A project may opt in** when its host model depends on exceptions, as a C++ test framework that reports a failed
+assertion by throwing does. The project says so in its documentation, keeps exceptions inside its own code, and still
+never lets one cross a C interface or escape a destructor. Third-party code keeps its own compiler options either way.
+
+## 7.2 `Expected<T>` and `Error`
+
+A function that produces a value or fails returns `Expected<T>`; one that only succeeds or fails returns `Error`. The guide
+does not prescribe the library. `llvm::Expected<T>` and `llvm::Error` suit code that already depends on LLVM; a C++20
+backport of `std::expected` (such as `tl::expected`) or a small type of the project's own suit the rest, and C++23's
+`std::expected` replaces them once a project moves to it. What matters is the discipline below: the failure is in the
+signature, it cannot be ignored, and it carries a message.
+
+The examples use LLVM's types.
+
+```cpp
+[[nodiscard]] auto
+ParseHeader(llvm::ArrayRef<std::uint8_t> bytes) -> llvm::Expected<Header>
+{
+ if (bytes.size() < kHeaderSize)
+ {
+ return llvm::createStringError(
+ "header is %zu bytes; at least %zu are required",
+ bytes.size(),
+ kHeaderSize);
+ }
+ // ...
+}
+
+[[nodiscard]] auto
+LoadModule(const std::filesystem::path &path) -> llvm::Expected<Module>
+{
+ auto bytes = ReadFile(path);
+ if (!bytes)
+ {
+ return bytes.takeError();
+ }
+ auto header = ParseHeader(*bytes);
+ if (!header)
+ {
+ // path::c_str() is wide on Windows; string() is the narrow form.
+ return llvm::joinErrors(
+ llvm::createStringError("%s: not a module",
+ path.string().c_str()),
+ header.takeError());
+ }
+ return Module{*header, std::move(*bytes)};
+}
+```
+
+- **Every result is inspected exactly once.** Prefer a type that enforces this: an `llvm::Error` that is destroyed
+ unchecked aborts a debug build, and `[[nodiscard]]` catches the rest. Propagate the error, handle it, or consume
+ it with a comment saying why it may be dropped.
+- **Add context on the way up**, so the final message says what was being done: the file, the record, the field. Do not
+ wrap an error without adding information.
+- **Messages are lower-case, without a trailing period**, and name the offending value: `entry 'Demo.Main' names no
+ class`. They are written for the person who has to fix the input.
+- **A typed error** (an error class or an error enumeration, such as a class derived from `llvm::ErrorInfo`) when a
+ caller needs to react to one kind differently, for example to retry. Otherwise an error with a message is enough.
+- **Do not use `std::error_code` as the interface** of new code. At the boundary of an API that reports through
+ `std::error_code`, such as the non-throwing `std::filesystem` overloads, convert it into the project's error
+ type (`llvm::errorCodeToError` with LLVM).
+ Always call the non-throwing overload; code built without exceptions cannot catch the other one.
+
+## 7.3 Invariants
+
+An invariant is something the program's own logic guarantees. A broken invariant is a bug in the program, not in its
+input, and there is no meaningful way to continue.
+
+- **`assert`** checks an invariant that is cheap to state, in debug builds. It never checks input, never has a side
+ effect, and its message is a condition a reader understands: `assert(index < blocks_.size() && "block id out of range")`.
+- **An always-on check** (a project macro that prints and calls `std::abort`, or `llvm::report_fatal_error`) guards
+ an invariant whose
+ violation would corrupt memory or produce wrong output in a release build, such as a verifier disagreeing with
+ itself. It prints a message and terminates.
+- **An unchecked unwrap** (`llvm::cantFail`, or `.value()` behind an assertion) takes the value of an `Expected` whose
+ success the caller has just established, such as an
+ operation on two sets that were created with the same size a line above. It is never a shortcut for input
+ validation.
+- **An unreachable marker** (a project macro, `llvm_unreachable`, or C++23's `std::unreachable` behind an assertion)
+ follows a `switch` that handles every enumerator; it is not a way to silence a missing case.
+
+The line between the two first kinds is the source of the value: **anything that comes from outside the process**
+(a file, the network, the command line, the environment, another component's serialized output) is validated and
+reported as a recoverable failure, never asserted.
+
+## 7.4 Fatal defects in tests and fuzzers
+
+A fuzz oracle that detects a defect, such as a round trip that changes a value, terminates with a fatal error so that
+the fuzzer keeps the input. A benchmark whose fixture is corrupt terminates as well; it must not time a broken
+workload. A test that cannot set up its fixture fails with a message, not a crash.
+
+## 7.5 Destructors and cleanup
+
+A destructor never fails. Cleanup that can fail, such as flushing a file, is a member function that returns `Error`
+and that the owner calls before the object goes away. The destructor then only releases what remains, silently.
+
+## 7.6 Diagnostics are not errors
+
+A compiler or a linter that reports problems in its input produces **diagnostics**: data with a severity, a location
+and a message, collected and printed by a sink. Producing a diagnostic is normal operation and does not use
+`Expected`. `Expected` is for the failure of an operation itself, such as being unable to read the input at all.