diff options
| author | Leitwolf11 | 2026-10-18 20:00:39 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-18 20:00:39 +0300 |
| commit | 8d1fb4807edc7920fbecd0554c9c61625827e19c (patch) | |
| tree | dd3d7e1d4f48ca27140a2bd5f970817ad02ae35a | |
| download | progmasoft-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>
| -rw-r--r-- | ADOPTING.md | 63 | ||||
| -rw-r--r-- | CHANGELOG.md | 18 | ||||
| -rw-r--r-- | CONTRIBUTING.md | 33 | ||||
| -rw-r--r-- | Guide/C-INTERFACES.md | 124 | ||||
| -rw-r--r-- | Guide/COMMENTS-AND-DOCUMENTATION.md | 82 | ||||
| -rw-r--r-- | Guide/CONCURRENCY.md | 73 | ||||
| -rw-r--r-- | Guide/ERRORS.md | 123 | ||||
| -rw-r--r-- | Guide/FILES-AND-HEADERS.md | 109 | ||||
| -rw-r--r-- | Guide/FORMATTING.md | 219 | ||||
| -rw-r--r-- | Guide/LANGUAGE.md | 170 | ||||
| -rw-r--r-- | Guide/NAMING.md | 94 | ||||
| -rw-r--r-- | Guide/OWNERSHIP-AND-MEMORY.md | 88 | ||||
| -rw-r--r-- | Guide/PRINCIPLES.md | 59 | ||||
| -rw-r--r-- | Guide/TESTING.md | 94 | ||||
| -rw-r--r-- | Guide/TOOLING.md | 161 | ||||
| -rw-r--r-- | Guide/TYPES-AND-CLASSES.md | 137 | ||||
| -rw-r--r-- | LICENSE.md | 418 | ||||
| -rw-r--r-- | LICENSES/CC0-1.0.md | 131 | ||||
| -rw-r--r-- | README.md | 61 |
19 files changed, 2257 insertions, 0 deletions
diff --git a/ADOPTING.md b/ADOPTING.md new file mode 100644 index 0000000..2fc3f12 --- /dev/null +++ b/ADOPTING.md @@ -0,0 +1,63 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# Adopting the guide + +A project adopts Progmasoft C++ Style by pointing to it, copying its two tool profiles and writing down, once, where it +deviates. + +## 1. Point to the guide + +Add a short section to the project's contributing guide: + +```markdown +## C++ style + +C++ code follows [Progmasoft C++ Style](https://github.com/Progmasoft/progmasoft-cpp). +`clang-format` and `clang-tidy` enforce what they can; review covers the rest. +Deviations of this project are listed below. +``` + +Do not copy the rules into the project. A copy drifts from the guide, and two documents that disagree are worse than +one. + +## 2. Copy the profiles + +| File | From | +| --- | --- | +| `.clang-format` | [Formatting: the profile](Guide/FORMATTING.md#the-profile) | +| `.clang-tidy` | [Tooling: the clang-tidy profile](Guide/TOOLING.md#132-the-clang-tidy-profile), with the path filters set to the project's directories | +| compiler flags | [Tooling: flags](Guide/TOOLING.md#flags), in the build files of the project | + +The profiles are in the public domain (see [LICENSE.md](LICENSE.md)), so they can be copied under the project's own +license. Note the guide revision they come from in a comment at the top of each file, so a later update can be compared. + +## 3. Record the deviations + +Every project has a few rules of its own: a namespace root, an include prefix, a test framework, a component that must +use exceptions because its host does. List them in the contributing guide, each with its reason: + +```markdown +### Deviations + +- The intermediate representations use the lowercase namespaces `tala::core`, `tala::xpp` and `tala::xmm`, because + they are qualified on almost every line of the compiler (Naming, 3.4). +- Assertion failures are reported by throwing, as the Catch2 API requires; exceptions stay inside the library + (Errors, 7.1). +``` + +Within the project, its documented rule wins over the guide. An undocumented difference is not a deviation; it is code +that has not caught up yet. + +## 4. Converge gradually + +Existing code is not rewritten in one sweep. A file follows the guide where it is changed. A purely mechanical change, +such as applying `clang-format` to a directory or renaming a member convention, is its own pull request, never mixed +with a change in behavior, so that review and `git blame` stay useful. + +## Projects that follow the guide + +| Project | Deviations recorded in | +| --- | --- | +| [Progmasoft/tala](https://github.com/Progmasoft/tala) | `Documents/Contributing/STYLE-AND-LICENSING.md` | +| [Progmasoft/catch3](https://github.com/Progmasoft/catch3) | the Progmasoft-authored sources under `src/Progmasoft/`; code derived from Catch2 keeps its upstream style | diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..feefc35 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# Changelog + +A revision of the guide is named by its date. A change that requires existing code to change says so. + +## Unreleased + +## 2026-10-18 + +The first revision of Progmasoft C++ Style. It writes down the conventions of the Progmasoft C++ projects and decides +what they left open: + +- thirteen chapters, from principles to tooling, with the `.clang-format` and `.clang-tidy` profiles and the compiler + flags in full; +- guidance for adopting the guide and recording a project's deviations; +- the text under CC BY 4.0, the configuration and code examples under CC0 1.0. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b718214 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# Contributing + +Progmasoft C++ Style changes when a rule turns out to be wrong, unclear or missing. Proposals are welcome from anyone, +whether or not they work on a Progmasoft project. + +## Proposing a change + +Open an issue first for anything beyond a correction of wording, a typo or a broken link. Describe: + +- **the rule**, as it reads now or as you would write it; +- **the problem**: code that the current rule makes worse, a bug it fails to prevent, or a case it does not cover; +- **the cost of the change** for projects that already follow the guide: how much code would have to change, and whether + a tool can do it. + +A rule earns its place by preventing a real class of mistakes or by removing a choice that would otherwise be argued +about in every review. "Another style guide does it" is a starting point, not a reason. + +## Writing the change + +- Keep the shape of the guide: a rule in bold that says what to do, then the reason, then an example when it helps. +- Examples follow every rule of the guide, including formatting by the profile and the 80-column limit. +- A change to `.clang-format` or `.clang-tidy` says which clang version it was tested with and what it changes in + formatted code. +- Write in US English, in the present tense and the active voice. +- Note the change under `## Unreleased` in [CHANGELOG.md](CHANGELOG.md), saying whether existing code must change. + +## License of contributions + +By contributing you agree that your text is licensed under [CC BY 4.0](LICENSE.md) and that your configuration and code +examples are dedicated to the public domain under [CC0 1.0](LICENSES/CC0-1.0.md), like the rest of the guide. diff --git a/Guide/C-INTERFACES.md b/Guide/C-INTERFACES.md new file mode 100644 index 0000000..3fc7b60 --- /dev/null +++ b/Guide/C-INTERFACES.md @@ -0,0 +1,124 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# 10. C interfaces + +A C interface is the stable binary boundary of a library: what another language, another compiler or another version of +the library links against. It is written in C11 and kept small; everything behind it is C++20. + +## 10.1 The header + +```c +// SPDX-FileCopyrightText: 2026 Example <contact@example.com> +// SPDX-License-Identifier: MPL-2.0 +#ifndef EXAMPLE_PARSER_H +#define EXAMPLE_PARSER_H + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/// Version of this interface; bumped on every incompatible change. +#define EXAMPLE_PARSER_ABI_VERSION 3 + +/// Opaque parser state, owned by the library. +typedef struct example_parser example_parser; + +/// Result of an operation of this interface. +typedef enum example_status +{ + EXAMPLE_STATUS_OK = 0, + EXAMPLE_STATUS_INVALID_ARGUMENT = 1, + EXAMPLE_STATUS_MALFORMED_INPUT = 2, +} example_status; + +/// Create a parser. Release it with example_parser_destroy. +/// @param out Receives the parser on success; untouched on failure. +example_status +example_parser_create(example_parser **out); + +/// Release a parser created by example_parser_create. Accepts NULL. +void +example_parser_destroy(example_parser *parser); + +#ifdef __cplusplus +} +#endif + +#endif // EXAMPLE_PARSER_H +``` + +- **`.h`, C11, and include guards.** A C header may be read by a C compiler that does not know `#pragma once`, so it uses + a guard named after the file. +- **Every name has the project prefix**: functions and types `example_`, macros and enumerators `EXAMPLE_`. C has one + namespace for the whole program. +- **`extern "C"`** guards wrap the declarations, so C++ callers link against the same symbols. +- **Opaque types**: callers see a forward-declared struct and a pointer, never the layout. The layout can then change + without breaking callers. +- **Fixed-width types** (`uint32_t`, `size_t` for sizes) and no `bool` in a struct whose layout is part of the ABI; use + `uint8_t` with documented values. +- **A version macro or a version function**, so a caller can check at build time or at run time that it talks to the + interface it was written for. + +## 10.2 Ownership across the boundary + +- **Whoever allocates, frees.** A buffer the library returns is released by a function of the library, never by the + caller's `free`, because the two may not share an allocator. +- **Borrowed data is copied before the call returns** when the library needs it longer. A callback that receives a + pointer and a length may use them only until it returns; the library copies anything it keeps into memory it owns. +- **Document every pointer parameter**: may it be null, who owns it after the call, how long it must stay valid. +- **No C++ type crosses the boundary**: no `std::string`, no reference, no class with a constructor, no template. + +## 10.3 Failure across the boundary + +- **No exception crosses a C interface**, ever. With exceptions disabled this holds by construction; a project that + enables them catches everything at the boundary. +- **Every fallible function returns a status** and writes its results through out-parameters, which it leaves untouched + on failure. A function that cannot fail returns `void` or the value. +- **Detailed messages** are available through a separate call or an error object the caller owns and releases, not + through a global "last error" that breaks with threads. + +## 10.4 The C++ side + +Behind the interface, the implementation is ordinary C++20 that follows the rest of this guide: + +```cpp +extern "C" auto +example_parser_create(example_parser **out) -> example_status +{ + if (out == nullptr) + { + return EXAMPLE_STATUS_INVALID_ARGUMENT; + } + auto parser = std::make_unique<example_parser>(); + *out = parser.release(); + return EXAMPLE_STATUS_OK; +} +``` + +C++ callers wrap a C handle in an RAII type right away: + +```cpp +struct ParserDeleter final +{ + void + operator()(example_parser *parser) const noexcept + { + example_parser_destroy(parser); + } +}; + +using ParserHandle = std::unique_ptr<example_parser, ParserDeleter>; +``` + +## 10.5 Keeping it stable + +- **Record the interface in one place**: a manifest or a header that is the single source of truth for every symbol, + struct layout and magic number, checked in CI against the code. +- **Add, do not change.** A new function or a new enumerator is compatible; changing a signature, a layout or the meaning + of a value is not, and bumps the interface version. +- **Test the interface from C**: a test translation unit compiled as C11 includes the header and calls every function, + so that a C++-only construct in the header cannot slip in. diff --git a/Guide/COMMENTS-AND-DOCUMENTATION.md b/Guide/COMMENTS-AND-DOCUMENTATION.md new file mode 100644 index 0000000..7cdffb7 --- /dev/null +++ b/Guide/COMMENTS-AND-DOCUMENTATION.md @@ -0,0 +1,82 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# 9. Comments and documentation + +Comments are written in US English, in full sentences that end with a period. They explain what the code cannot say for +itself: what an interface promises, why a choice was made, and what would break if someone changed it. + +## 9.1 Documentation comments + +Every public declaration in a header, meaning every type, function, enumerator, constant and data member that another +component can use, has a Doxygen comment written with `///`: + +```cpp +/// Derive the blocks that are live on entry to each block of a function. +/// The result lists blocks in ascending id order, so it is stable across +/// runs and can be compared in tests. +/// @param graph Control-flow graph of the function; must be verified. +/// @param uses Variables read by each block. +/// @return Live-in sets, or an error when the graph and the use table +/// describe different numbers of blocks. +[[nodiscard]] auto +AnalyzeLiveness(const ControlFlowGraph &graph, const UseTable &uses) + -> llvm::Expected<LivenessResult>; +``` + +- **The first sentence says what the declaration is or does**, as a summary that stands on its own in a list of + declarations. Start a function's summary with a verb in the imperative: "Derive", "Return", "Create". Start a type's + summary with a noun phrase. +- **Then the contract**: preconditions, what the result guarantees (order, ownership, lifetime of returned views), what + happens on failure, and whether the function is thread-safe when that is not obvious. +- **`@param`** for every parameter whose meaning or constraints the name does not fully convey, and **`@return`** for + every non-void function. `@pre`, `@post` and `@note` may be used where they help. +- **Do not document the implementation** in a header. A comment that says "uses a hash map internally" is wrong the day + the implementation changes, and the caller should not depend on it. +- **A longer description** may use a `/** ... */` block with `@brief`, lists and examples, as Doxygen renders them. +- **Doxygen runs in CI** with warnings as errors for the public headers, so an undocumented or misdocumented declaration + fails the build. + +## 9.2 Implementation comments + +Inside a function, comment **why**, not what: + +```cpp +// The verifier already rejected unreachable blocks, so every block here has a +// predecessor and the entry block is the only root of the dominator tree. +``` + +```cpp +// Avoid: restates the code. +// Increment the counter. +++counter; +``` + +Comment especially: + +- an invariant the code relies on and a later change could break: identity, ordering, ownership, a protocol bound; +- a workaround for a compiler, library or platform defect, with a link to the report and the condition for removing it; +- a choice that looks wrong but is deliberate, and why the obvious alternative does not work; +- the reason for every magic number that cannot be a named constant. + +Keep a comment next to the code it explains, and change it in the same commit as the code. + +## 9.3 TODOs + +A `TODO` names the issue that tracks it: `// TODO(#123): accept signed offsets once the wire format has them.` A TODO +without an issue is either done now or not written. Do not leave commented-out code; version control keeps the history. + +## 9.4 File comments + +A file starts with its license header and has no other boilerplate: no author list, no creation date, no change log. +When a file needs an overview, such as a component's main header, write it as the documentation comment of the +namespace or of the main type. + +## 9.5 Words + +- Say what a thing is for, in the vocabulary of its users, not of its implementation. +- Prefer the active voice and the present tense: "The parser stops at the first error", not "The parsing will be + stopped". +- Use the project's terms consistently. If the project calls something a "block", do not call it a "node" in one comment + and a "basic block" in another. +- Do not use comments for jokes, blame or emotions. A comment is read years later, out of context, by someone in a hurry. diff --git a/Guide/CONCURRENCY.md b/Guide/CONCURRENCY.md new file mode 100644 index 0000000..9099de3 --- /dev/null +++ b/Guide/CONCURRENCY.md @@ -0,0 +1,73 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# 11. Concurrency + +Concurrent code is correct only when it is correct for every interleaving, and testing finds few of them. The rules +below make the shared state small and visible, so that a reader can check it. + +## 11.1 Share less + +- **Prefer no sharing**: give each thread its own data and combine the results at the end. A result that several + threads compute is merged in a fixed order, so the output does not depend on scheduling. +- **Immutable data may be shared freely.** An object that is fully built before the threads start and never changed + afterwards needs no lock. +- **Everything else that more than one thread touches** is protected by one mutex, or is an atomic, and the comment of + the member says which. + +## 11.2 Threads + +- **`std::jthread`**, which joins on destruction and carries a stop token, instead of `std::thread`. +- **No detached threads.** Every thread has an owner that joins it. +- **A thread pool or an executor** for many short tasks, owned by the component that needs it, not a global. +- **Name long-lived threads** when the platform allows it, so they are recognizable in a debugger and a profiler. + +## 11.3 Locks + +- **`std::scoped_lock`** to lock, and `std::unique_lock` only when a condition variable or a deferred lock needs it. + Never call `lock()` and `unlock()` by hand. +- **Hold a lock for as short as possible**, and never while calling code you do not control: a callback, a virtual + function of an unknown implementation, I/O. +- **Lock several mutexes with one `std::scoped_lock`**, which avoids deadlock, or document a lock order and keep to it. +- **A condition variable waits with a predicate**: `condition.wait(lock, [&] { return ready; })`, never a bare `wait`, + which wakes spuriously. + +## 11.4 Thread-safety annotations + +With Clang, annotate the shared state and the functions that need a lock, and build with `-Wthread-safety` as an error: + +```cpp +class Cache final +{ +public: + void + Insert(Key key, Value value) EXAMPLE_EXCLUDES(mutex_); + +private: + Mutex mutex_; + std::unordered_map<Key, Value> entries_ EXAMPLE_GUARDED_BY(mutex_); +}; +``` + +The annotation macros wrap Clang's `capability`, `guarded_by`, `requires_capability` and `locks_excluded` attributes, +and expand to nothing on other compilers. `Mutex` is a thin wrapper of `std::mutex` that carries the `capability` +attribute, because the standard type does not. + +## 11.5 Atomics + +- **An atomic is for one value**, such as a counter or a flag. Two atomics that must change together need a mutex. +- **Sequentially consistent by default**: `counter.fetch_add(1)`. A weaker memory order (`acquire`, `release`, + `relaxed`) needs a comment that names the operation it synchronizes with and why the weaker order is enough. +- **No `volatile` for synchronization.** It is for memory-mapped hardware, not for threads. + +## 11.6 Statics and globals + +- A function-local `static` that is initialized once is thread-safe in C++; use it for a lazily built constant table. + Do not use it for mutable state. +- No mutable global state. When a process-wide resource is unavoidable, such as a signal handler or a C library that + must be initialized once, wrap it in one object with a documented lifetime. + +## 11.7 Testing concurrent code + +ThreadSanitizer runs the concurrent tests in CI (see [Testing](TESTING.md#123-sanitizers)). A test that relies on +`sleep` to order threads is wrong; it uses a latch, a barrier or a condition the threads wait for. 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. diff --git a/Guide/FILES-AND-HEADERS.md b/Guide/FILES-AND-HEADERS.md new file mode 100644 index 0000000..579be87 --- /dev/null +++ b/Guide/FILES-AND-HEADERS.md @@ -0,0 +1,109 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# 4. Files and headers + +## 4.1 Extensions + +| Extension | Holds | +| --- | --- | +| `.hpp` | a C++ header | +| `.cpp` | a C++ translation unit | +| `.h` | a C11 header for a [C interface](C-INTERFACES.md), and nothing else | +| `.inc` | a fragment that is included more than once on purpose, such as an X-macro table | + +A C++-only header never uses `.h`: the extension tells a reader, and a build tool, which language the file is. + +## 4.2 The layout of a header + +```cpp +// SPDX-FileCopyrightText: 2026 Example <contact@example.com> +// SPDX-License-Identifier: MPL-2.0 +#pragma once + +#include <cstdint> +#include <vector> + +#include <llvm/Support/Error.h> + +#include "Example/Graph/BlockId.hpp" + +namespace Example::Graph +{ + /// ... +} // namespace Example::Graph +``` + +1. The license header, as two SPDX comment lines, when the project uses them. +2. `#pragma once`. Every major compiler supports it, and it cannot collide the way two include guards with the same + macro name can. +3. The includes (see below). +4. One namespace block. `clang-format` adds the closing comment. + +A translation unit has the same shape without `#pragma once`. + +## 4.3 Includes + +- **Include what you use.** A file includes the header of every name it uses directly, even when another include + happens to bring it in. A header that compiles only because of what was included before it is broken. +- **Every header compiles on its own.** A build that compiles each public header in a translation unit of its own + catches a missing include before a user does. +- **Angle brackets for code you do not own, quotes for code you do.** `<vector>`, `<llvm/ADT/StringRef.h>` and the + headers of every dependency use `<>`; the project's own headers use `""`. `clang-format` then puts the two groups + in that order and sorts each. +- **Spell a project include from the root of its include path**: `"Example/Graph/BlockId.hpp"`, never a relative path + such as `"../BlockId.hpp"`. The spelling then names the same file from every directory. +- **Public headers live under a directory named for the project**, such as `Headers/Example/Graph/`, so that the + include spelling starts with the project name and cannot clash with another library's headers. +- **Forward-declare** a project class in a header that only needs its name for a pointer or reference, to keep build + dependencies small. Do not forward-declare names from the standard library or from another project; include their + header instead. +- **No `using namespace`**, in a header or a translation unit, at any scope. A `using` declaration of one name, a type + alias or a namespace alias is fine inside a function or inside the project's own namespace in a `.cpp`: + + ```cpp + namespace + { + namespace Graph = Example::Graph; + } // namespace + ``` + +## 4.4 What goes in a header + +A header declares the interface of a component: types, function declarations, constants, and the inline functions and +templates that must be visible to callers. Everything else goes in the `.cpp`. + +- A function is defined in the header only when it is a template, `constexpr`, or a one-line accessor that the type + definition reads better with. +- Helpers used by one translation unit go in an unnamed namespace in that `.cpp`, giving them internal linkage. Do not use + `static` for that purpose at namespace scope. +- A header defines no non-`inline` variable. A constant in a header is `inline constexpr`. +- A header does not depend on a macro being defined before it is included, except for the configuration macros the + project documents. + +## 4.5 The order inside a class + +Order a class by what a reader of its interface needs first: + +1. `public`, then `protected`, then `private`. +2. Within each: types and aliases, then constants, then factories and constructors, then the destructor, then the special + members, then other member functions, then data members. + +Data members are always last and always `private` in a class (see [Types and classes](TYPES-AND-CLASSES.md)). + +## 4.6 Size + +A file is at most 1500 lines; 750 or fewer is the target. The limit is not a reason to cut a coherent unit at an +arbitrary line. Split by responsibility instead, such as model, verifier, lowering, diagnostics, I/O. A file that grows +past the target is a hint that it does two things. + +A function should fit on a screen, about 60 lines. One that does not usually has steps that deserve names of their own. + +There is no `Utils.hpp`, `Common.hpp` or `Helpers.cpp`. A helper belongs with the code that uses it, or in a component +named for what the helper does. + +## 4.7 Generated and vendored code + +Generated sources are written into the build output, not into the source tree, and are never edited by hand. Vendored +code lives under one directory, such as `third_party/`, keeps its own license, formatting and style, and is excluded +from formatting and linting. A change to vendored code is made upstream, or recorded as a patch with its reason. diff --git a/Guide/FORMATTING.md b/Guide/FORMATTING.md new file mode 100644 index 0000000..80a355c --- /dev/null +++ b/Guide/FORMATTING.md @@ -0,0 +1,219 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# 2. Formatting + +Formatting is mechanical, so a tool does it. Every C++ file is formatted by `clang-format` with the profile below, and +CI fails when a file is not. Nobody formats by hand, and nobody argues about a formatting choice in a review: a change +to the profile is a change to this guide. + +## The shape of the code + +```cpp +namespace Example::Geometry +{ + /// Axis-aligned rectangle with a non-negative size. + class Rectangle final + { + public: + /// Create a rectangle, or report why the size is not valid. + /// @param width Width in pixels; must not be negative. + /// @param height Height in pixels; must not be negative. + /// @return The rectangle, or an error naming the invalid side. + [[nodiscard]] static auto + Create(std::int32_t width, std::int32_t height) + -> llvm::Expected<Rectangle>; + + /// Area in square pixels. + [[nodiscard]] auto + Area() const noexcept -> std::int64_t + { + return static_cast<std::int64_t>(width_) * height_; + } + + private: + Rectangle(std::int32_t width, std::int32_t height) + : width_{width} + , height_{height} + |