Split the README reference material into docs/
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / coverage (push) Successful in 2m48s
libakerror CI Build / thread_sanitizer (push) Failing after 2m49s
libakerror CI Build / mutation_test (push) Successful in 39m56s

The README was 794 lines: the summary, the design rationale, the whole macro
reference, the threading contract, the build internals and the exit-status
specification in one file. It is now 178 lines -- summary, installation,
quickstart, and an index -- and the reference material lives in docs/, one file
per topic: architecture, usage, status-codes, uncaught-errors, exit-status,
thread-safety, building.

The prose moved as written. Inbound references followed it: UPGRADING.md,
TODO.md, include/akerror.tmpl.h and tests/err_threads_handoff.c now name the
docs/ file that owns the text they cite, and AGENTS.md says where new
documentation goes so the README does not grow back.

Five factual errors fixed in the moved text:

- Both NULL-pointer examples inverted their test. FAIL_ZERO_* fails when the
  expression is zero, so `(somePointer == NULL)` failed on a *valid* pointer.
  They now read `(somePointer != NULL)`.
- AKERROR_NOIGNORE, four times including the #define, is AKERR_NOIGNORE.
- FINISH_NORExbTURN is FINISH_NORETURN.
- "functiions" is "functions".
- The architecture link pointed at include/akerror.h, which is generated and
  not in the tree; it points at include/akerror.tmpl.h.

The quickstart is new text. It compiles under -Wall -Wextra -Werror and was run
through all three of its paths: handled usage error exits 0, unhandled IO error
prints a trace and exits with the status, success exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 16:56:07 -04:00
parent 9bf8bcd357
commit 5695061130
13 changed files with 863 additions and 725 deletions

48
docs/architecture.md Normal file
View File

@@ -0,0 +1,48 @@
# Library Architecture
## Philosophy of Use
This library has 6 guiding principles:
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
* Functions should return rich descriptive error contexts, not values
* Uncaught errors should cause program termination with a stacktrace
* Dynamic memory allocation is the source of many errors and should be avoided if possible
* Manipulating the call stack directly is error prone and dangerous
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
## Lifecycle of an error in the AKError library
TL;DR - `akerr_ErrorContext` objects are filled with error context information and bubbled up through nested control structures until they are handled or reach the top level, where an unhandled error halts program termination with a stack trace
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
2. The akerr_ErrorContext is returned from the scope where the error was detected
3. The akerr_ErrorContext enters a control structure provided by the AKError library through a series of macros that examine `akerr_ErrorContext` objects as they pass through
4. The control structure checks to see if the `akerr_ErrorContext` has an error set, and if so, if there are any handlers in the current control structure that can handle it
5. If the current control structure can handle the `akerr_ErrorContext`, it does so
6. If the current control structure can not handle the `akerr_ErrorContext`, then the current control structure's cleanup code (if any) is executed, and the `akerr_ErrorContext` object is passed out of the current control structure to the parent control structure
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
8. When the first level of the control structure is reached, if the `akerr_ErrorContext` has an error set in it, then the stack trace information in the `akerr_ErrorContext` object is used to print a stack trace using the configured logging function, and program termination is halted
## What is in an Error Context
The Error Context object is a simple object which contains a few things:
* A numeric error code
* The name of the file in which the error occurred
* The name of the function in which the error occurred
* The line number in the file at which the error occurred
* A character buffer containing a message about the error in question
The structure also contains housekeeping information for the library which are of no specific interest to the user. See [include/akerror.tmpl.h](../include/akerror.tmpl.h) for more details.
## What are the control structures
The library is structured around a series of macros that construct `switch` statements that perform logic against an `akerr_ErrorContext` which exists in the current scope and has been initialized. These macros must be assembled in a specific order to produce a syntactically correct `switch` statement which performs correct operations against the `akerr_ErrorContext` to attempt operations, detect failures, perform cleanup operations, handle errors, and then exit a given scope in a success or failure state.
## Functions and Return Codes
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *`.
Any function which uses the `PREPARE_ERROR` macro should have a return type of `akerr_ErrorContext *`. The macros within this library, when they detect an unhandled error, will attempt to pass up the unhandled error to the context of the previous function in the call stack. This allows for errors to propagate up through the call stack in the same way as exceptions. (For example, if you use traditional C error handling in a call stack of `a() -> b() -> c()`, and `c()` fails because it runs out of memory, `b()` will likely detect that error and return some error to `a()`, but it may or may not return the context of what failed and why. With this, you get that context all the way up in `a()` without knowing anything about `c()`.