2026-07-30 13:22:50 -04:00
|
|
|
# TODO
|
|
|
|
|
|
2026-07-30 18:19:53 -04:00
|
|
|
Working notes for `libakerror`. Outstanding items only.
|
|
|
|
|
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
## 1. Only ThreadSanitizer is wired into CI, not ASan/UBSan
|
2026-07-30 18:19:53 -04:00
|
|
|
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
`AKERR_SANITIZE` builds the library and the tests with any sanitizer list, and
|
|
|
|
|
CI runs `-DAKERR_SANITIZE=thread` through `scripts/thread_test.sh`. Nothing runs
|
|
|
|
|
`address,undefined` yet, and that is the one that covers the original
|
|
|
|
|
motivation: mutation testing caught an out-of-bounds probe in the status-name
|
|
|
|
|
hash table that the suite could not, because the failure mode was a write into
|
|
|
|
|
adjacent BSS, which does not crash. Sharpening one test closed that instance;
|
|
|
|
|
ASan would catch the whole class regardless of how sharp the assertions are.
|
2026-07-30 18:19:53 -04:00
|
|
|
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
The machinery is in place — this is one more job in
|
|
|
|
|
`.gitea/workflows/ci.yaml` running
|
|
|
|
|
`cmake -S . -B build/asan -DAKERR_SANITIZE=address,undefined`. Left separate
|
|
|
|
|
because ASan and TSan cannot be combined in one build.
|
2026-07-30 18:19:53 -04:00
|
|
|
|
|
|
|
|
## 2. `HANDLE`-level status aliasing is still undetectable
|
|
|
|
|
|
|
|
|
|
Two components can compile the same integer into a `case` label without ever
|
|
|
|
|
reserving a range or registering a name, and nothing sees it. Ownership
|
|
|
|
|
enforcement covers *naming*, which is the part the library mediates; the `case`
|
|
|
|
|
label never reaches it.
|
|
|
|
|
|
|
|
|
|
Closing this needs the `if`/`else if` handler ladder — rewriting
|
|
|
|
|
`PROCESS`/`HANDLE`/`HANDLE_GROUP`/`HANDLE_DEFAULT`/`FINISH` so status matching
|
|
|
|
|
is not restricted to integer constant expressions. That would also allow
|
|
|
|
|
matching on ranges or predicates, and would let a handler resolve a code through
|
|
|
|
|
its owner. It touches the most load-bearing code in the library and every
|
|
|
|
|
consumer's error handling at once, so it wants its own change.
|
|
|
|
|
|
|
|
|
|
Note it would *not* by itself fix the "don't use `CATCH` or `FAIL_*_BREAK`
|
|
|
|
|
inside a loop" hazard: that comes from exiting via `break`, not from `switch`.
|
|
|
|
|
|
|
|
|
|
## 3. No registry introspection
|
|
|
|
|
|
|
|
|
|
There is no way to ask who owns a status, or to enumerate reservations. The
|
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
|
|
|
"coordinate ranges at the dependency-stack level" advice in UPGRADING.md
|
|
|
|
|
therefore has no tooling behind it.
|
2026-07-30 18:19:53 -04:00
|
|
|
|
|
|
|
|
A read-only accessor plus a dump through `akerr_log_method` would let a startup
|
|
|
|
|
self-check or a CI job print the whole map for a linked stack. Cheap, additive,
|
|
|
|
|
and the natural next step for multi-component adoption.
|
|
|
|
|
|
|
|
|
|
## 4. No way to release a reservation
|
|
|
|
|
|
|
|
|
|
A plugin host that `dlopen`s many distinct plugins over a process lifetime
|
|
|
|
|
accumulates ranges until the table fills. Reloading the *same* plugin is fine —
|
|
|
|
|
an identical repeat by the same owner is idempotent.
|
|
|
|
|
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
## 5. Renaming a status is not safe against a concurrent lookup
|
2026-07-30 18:19:53 -04:00
|
|
|
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
`akerr_name_for_status(status, NULL)` returns a pointer into the registry rather
|
|
|
|
|
than a copy, which is what makes it usable from inside `FAIL` — it needs no
|
|
|
|
|
buffer and no error context of its own. Registering a *second* name for a status
|
|
|
|
|
that already has one (`tests/err_name_ownership.c` covers that it is allowed)
|
|
|
|
|
overwrites that buffer in place, so a thread reading the name at that moment can
|
|
|
|
|
see a torn string. Every other registry operation is serialized; this one cannot
|
|
|
|
|
be, because the reader is outside the lock by the time it reads the characters.
|
|
|
|
|
|
Split the README reference material into docs/
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>
2026-08-01 16:56:07 -04:00
|
|
|
Documented in docs/thread-safety.md and UPGRADING.md as "register names during
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
initialization". Closing it properly means making a registered name immutable —
|
|
|
|
|
either refusing a rename outright (a behavior change, and
|
|
|
|
|
`tests/err_name_ownership.c` asserts the current contract), or copying names
|
|
|
|
|
into a bump-allocated arena and publishing the pointer with a release store, so
|
|
|
|
|
a rename allocates new storage instead of rewriting live storage. The arena is
|
|
|
|
|
the better answer; it costs a second capacity limit and its exhaustion path.
|
2026-07-30 18:19:53 -04:00
|
|
|
|
|
|
|
|
## 6. Deprecate the two-argument name-registration path
|
|
|
|
|
|
|
|
|
|
`akerr_name_for_status(status, name)` cannot identify its caller, so it can only
|
|
|
|
|
check that *some* reservation covers the status, not that the caller owns it. It
|
|
|
|
|
exists for migration. Once consumers have moved to
|
|
|
|
|
`akerr_register_status_name()`, make the set path a no-op or remove it and leave
|
|
|
|
|
`akerr_name_for_status()` as pure lookup.
|
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
|
|
|
|
Use the library's own error idioms inside the library
Four things in src/error.c did by hand what the macros already do, or
skipped checks the library would have caught for a consumer.
akerr_copy_string() returned void and validated only its capacity, while
writing through a caller-supplied pointer for a caller-supplied length.
It is now __akerr_copy_string() and raises: AKERR_NULLPOINTER for a NULL
destination or source, AKERR_VALUE for a capacity with no room for a
terminator. Both call sites PASS it, and the owner copy in
akerr_reserve_status_range() now gates the commit, so a failed copy
cannot leave a range claimed under an empty owner. It is exported under
the internal prefix rather than static so tests/err_copy_string.c can
drive those guards; nothing else can reach them.
__akerr_name_library_status() and the band reservation in akerr_init()
hand-rolled the log/handler/release sequence. Both now use
ATTEMPT/CATCH/PROCESS/FINISH_NORETURN. PASS does not fit: both sites are
void and have no caller to propagate to, so the terminal form of the same
idiom is the right one -- an unhandled failure prints its stack trace and
goes to akerr_handler_unhandled_error, which terminates, exactly as
before but without the bespoke plumbing. The legacy set path in
akerr_name_for_status() had the same shape and now handles its refusal
with HANDLE_DEFAULT, converting it to the "Unknown Error" sentinel.
Every remaining `if (x) { FAIL_RETURN }` in the registry is now
FAIL_ZERO_RETURN or FAIL_NONZERO_RETURN, and akerr_register_status_name()
checks both owner and name before passing either down --
akerr_store_status_name() reads a NULL owner as "caller did not identify
itself" for the legacy path, so a NULL arriving through the owned entry
point would have skipped the ownership check entirely.
New tests: err_copy_string (the guards above), err_library_status_fatal
(WILL_FAIL -- proves a refused library-status registration terminates).
Tests: ctest 31/31, mutation 80.7% (was 77.5%), line coverage 98.9%.
Branch coverage on src/error.c drops 64.5% -> 50.4%, just over its gate:
each FAIL_* site carries ~6 branch outcomes of error-construction
machinery that only run when that failure fires, and each PASS around a
call that cannot fail carries ~25, so added validation lowers the ratio
by construction. Recorded in TODO.md item 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:53:33 -04:00
|
|
|
## 7. `akerr_init()`'s own reservation failure is untested
|
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
|
|
|
|
Use the library's own error idioms inside the library
Four things in src/error.c did by hand what the macros already do, or
skipped checks the library would have caught for a consumer.
akerr_copy_string() returned void and validated only its capacity, while
writing through a caller-supplied pointer for a caller-supplied length.
It is now __akerr_copy_string() and raises: AKERR_NULLPOINTER for a NULL
destination or source, AKERR_VALUE for a capacity with no room for a
terminator. Both call sites PASS it, and the owner copy in
akerr_reserve_status_range() now gates the commit, so a failed copy
cannot leave a range claimed under an empty owner. It is exported under
the internal prefix rather than static so tests/err_copy_string.c can
drive those guards; nothing else can reach them.
__akerr_name_library_status() and the band reservation in akerr_init()
hand-rolled the log/handler/release sequence. Both now use
ATTEMPT/CATCH/PROCESS/FINISH_NORETURN. PASS does not fit: both sites are
void and have no caller to propagate to, so the terminal form of the same
idiom is the right one -- an unhandled failure prints its stack trace and
goes to akerr_handler_unhandled_error, which terminates, exactly as
before but without the bespoke plumbing. The legacy set path in
akerr_name_for_status() had the same shape and now handles its refusal
with HANDLE_DEFAULT, converting it to the "Unknown Error" sentinel.
Every remaining `if (x) { FAIL_RETURN }` in the registry is now
FAIL_ZERO_RETURN or FAIL_NONZERO_RETURN, and akerr_register_status_name()
checks both owner and name before passing either down --
akerr_store_status_name() reads a NULL owner as "caller did not identify
itself" for the legacy path, so a NULL arriving through the owned entry
point would have skipped the ownership check entirely.
New tests: err_copy_string (the guards above), err_library_status_fatal
(WILL_FAIL -- proves a refused library-status registration terminates).
Tests: ctest 31/31, mutation 80.7% (was 77.5%), line coverage 98.9%.
Branch coverage on src/error.c drops 64.5% -> 50.4%, just over its gate:
each FAIL_* site carries ~6 branch outcomes of error-construction
machinery that only run when that failure fires, and each PASS around a
call that cannot fail carries ~25, so added validation lowers the ratio
by construction. Recorded in TODO.md item 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:53:33 -04:00
|
|
|
`tests/err_library_status_fatal.c` covers the terminal path in
|
|
|
|
|
`__akerr_name_library_status()` by naming a status the library does not own. The
|
|
|
|
|
band reservation in `akerr_init()` has no such handle: it can only fail in a
|
|
|
|
|
build whose tables are too small for the library's own entries, and both sizes
|
|
|
|
|
are `PRIVATE` to the library target, so a test executable cannot set them.
|
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
|
|
|
|
Use the library's own error idioms inside the library
Four things in src/error.c did by hand what the macros already do, or
skipped checks the library would have caught for a consumer.
akerr_copy_string() returned void and validated only its capacity, while
writing through a caller-supplied pointer for a caller-supplied length.
It is now __akerr_copy_string() and raises: AKERR_NULLPOINTER for a NULL
destination or source, AKERR_VALUE for a capacity with no room for a
terminator. Both call sites PASS it, and the owner copy in
akerr_reserve_status_range() now gates the commit, so a failed copy
cannot leave a range claimed under an empty owner. It is exported under
the internal prefix rather than static so tests/err_copy_string.c can
drive those guards; nothing else can reach them.
__akerr_name_library_status() and the band reservation in akerr_init()
hand-rolled the log/handler/release sequence. Both now use
ATTEMPT/CATCH/PROCESS/FINISH_NORETURN. PASS does not fit: both sites are
void and have no caller to propagate to, so the terminal form of the same
idiom is the right one -- an unhandled failure prints its stack trace and
goes to akerr_handler_unhandled_error, which terminates, exactly as
before but without the bespoke plumbing. The legacy set path in
akerr_name_for_status() had the same shape and now handles its refusal
with HANDLE_DEFAULT, converting it to the "Unknown Error" sentinel.
Every remaining `if (x) { FAIL_RETURN }` in the registry is now
FAIL_ZERO_RETURN or FAIL_NONZERO_RETURN, and akerr_register_status_name()
checks both owner and name before passing either down --
akerr_store_status_name() reads a NULL owner as "caller did not identify
itself" for the legacy path, so a NULL arriving through the owned entry
point would have skipped the ownership check entirely.
New tests: err_copy_string (the guards above), err_library_status_fatal
(WILL_FAIL -- proves a refused library-status registration terminates).
Tests: ctest 31/31, mutation 80.7% (was 77.5%), line coverage 98.9%.
Branch coverage on src/error.c drops 64.5% -> 50.4%, just over its gate:
each FAIL_* site carries ~6 branch outcomes of error-construction
machinery that only run when that failure fires, and each PASS around a
call that cannot fail carries ~25, so added validation lowers the ratio
by construction. Recorded in TODO.md item 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:53:33 -04:00
|
|
|
Closing it means a second library target built with tiny tables plus a
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
`WILL_FAIL` test linked against it. Nothing in the CMake does that yet: the
|
|
|
|
|
sanitizer and coverage options vary the *flags* of the one library target, not
|
|
|
|
|
its compile definitions.
|
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
|
|
|
|
Use the library's own error idioms inside the library
Four things in src/error.c did by hand what the macros already do, or
skipped checks the library would have caught for a consumer.
akerr_copy_string() returned void and validated only its capacity, while
writing through a caller-supplied pointer for a caller-supplied length.
It is now __akerr_copy_string() and raises: AKERR_NULLPOINTER for a NULL
destination or source, AKERR_VALUE for a capacity with no room for a
terminator. Both call sites PASS it, and the owner copy in
akerr_reserve_status_range() now gates the commit, so a failed copy
cannot leave a range claimed under an empty owner. It is exported under
the internal prefix rather than static so tests/err_copy_string.c can
drive those guards; nothing else can reach them.
__akerr_name_library_status() and the band reservation in akerr_init()
hand-rolled the log/handler/release sequence. Both now use
ATTEMPT/CATCH/PROCESS/FINISH_NORETURN. PASS does not fit: both sites are
void and have no caller to propagate to, so the terminal form of the same
idiom is the right one -- an unhandled failure prints its stack trace and
goes to akerr_handler_unhandled_error, which terminates, exactly as
before but without the bespoke plumbing. The legacy set path in
akerr_name_for_status() had the same shape and now handles its refusal
with HANDLE_DEFAULT, converting it to the "Unknown Error" sentinel.
Every remaining `if (x) { FAIL_RETURN }` in the registry is now
FAIL_ZERO_RETURN or FAIL_NONZERO_RETURN, and akerr_register_status_name()
checks both owner and name before passing either down --
akerr_store_status_name() reads a NULL owner as "caller did not identify
itself" for the legacy path, so a NULL arriving through the owned entry
point would have skipped the ownership check entirely.
New tests: err_copy_string (the guards above), err_library_status_fatal
(WILL_FAIL -- proves a refused library-status registration terminates).
Tests: ctest 31/31, mutation 80.7% (was 77.5%), line coverage 98.9%.
Branch coverage on src/error.c drops 64.5% -> 50.4%, just over its gate:
each FAIL_* site carries ~6 branch outcomes of error-construction
machinery that only run when that failure fires, and each PASS around a
call that cannot fail carries ~25, so added validation lowers the ratio
by construction. Recorded in TODO.md item 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:53:33 -04:00
|
|
|
Related: branch coverage on `src/error.c` now sits just above its 50% gate.
|
|
|
|
|
Every `FAIL_*` site carries about six branch outcomes of error-construction
|
|
|
|
|
machinery (`ENSURE_ERROR_READY`, `AKERR_STACKTRACE_APPEND`) that only run when
|
|
|
|
|
that specific failure fires, and every `PASS` site around a call that cannot
|
|
|
|
|
fail carries about twenty-five. Validating more inputs therefore lowers the
|
|
|
|
|
ratio by construction. Before adding defensive checks, expect to add a test that
|
|
|
|
|
drives them, as `tests/err_copy_string.c` does.
|
|
|
|
|
|
2026-07-31 08:45:20 -04:00
|
|
|
## 8. Mutation testing judges concurrency mutants without a sanitizer
|
|
|
|
|
|
|
|
|
|
`scripts/mutation_test.py` configures each mutant build with the default CMake
|
|
|
|
|
options, so a mutant that only breaks under concurrency is judged by a suite
|
|
|
|
|
running without ThreadSanitizer. Deleting the pool's `akerr_mutex_lock()` call
|
|
|
|
|
survives the run even though it is a real race: rebuilt and run directly, that
|
|
|
|
|
mutant fails `tests/err_threads_pool.c` in 4 of 10 runs, and fails under
|
|
|
|
|
`scripts/thread_test.sh` in 5 of 5. So 81.2% is a floor for that category, not a
|
|
|
|
|
verdict.
|
|
|
|
|
|
|
|
|
|
Closing it means a `--cmake-arg` passthrough on the harness so the mutant build
|
|
|
|
|
can be configured with `-DAKERR_SANITIZE=thread`. The whole run then costs a
|
|
|
|
|
TSan-instrumented suite per mutant (roughly 6s instead of 0.4s), so it belongs
|
|
|
|
|
behind a flag rather than in the default target or in CI.
|
|
|
|
|
|
Document and test handing an error context between threads
The thread-safety section filed two different things under "does not cover,
and cannot": sharing a context between threads, and passing one to another
thread. Only the first is unsupported. Transfer already works by
construction -- the reference count is the only field the library reads
across an ownership boundary, and it is only ever touched under the pool
lock, so akerr_release_error() does not care which thread checked the slot
out. The pool is process-global, not thread-local, so a context outlives the
thread that raised it.
Calling that unsupported told readers the worker/collector shape was off the
table, which either cost them the pattern or cost them the stack trace when
they rolled their own struct instead.
Split the bullet: transfer joins the covered list and gets its own section
with the rule, the worked pattern, and the four receiving-side hazards
(PREPARE_ERROR cannot adopt, CATCH assigns over the pointer, FINISH in a
void helper still parses its return, and an unhandled error now terminates
from the collector's thread). Sharing keeps the "cannot" bullet, narrowed to
what it actually is.
err_threads_handoff.c proves it: the existing thread tests all keep every
context on the thread that raised it, so the transfer path was exercised
nowhere. Seven producers hand errors to one collector through a bounded
mutex/condvar queue -- the mutex is the thing under test, since it is what
publishes the unlocked content writes -- and the collector asserts the
context is still a live slot at refcount 1, that message and trace arrive
whole and in each producer's order, that the slot was never recycled in
flight, and that a thread which never called akerr_next_error() can release
it. A second phase reads a context whose raising thread has already exited.
Also document why copying a context by assignment is silently wrong:
stacktracebufptr is self-referential, so the copy's cursor points into the
source's buffer and the first append corrupts a slot the copier no longer
owns. TODO.md records the akerr_copy_error() shape that would fix it and the
trigger for building it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:35:18 -04:00
|
|
|
## 9. No way to keep an error context and report it at the same time
|
|
|
|
|
|
Split the README reference material into docs/
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>
2026-08-01 16:56:07 -04:00
|
|
|
A context can be handed to another thread and released there --
|
|
|
|
|
`docs/thread-safety.md` now documents that pattern and
|
|
|
|
|
`tests/err_threads_handoff.c` proves it -- but it is a *move*. A thread that
|
|
|
|
|
wants to both keep its error and report it upward has to read the fields out
|
|
|
|
|
into its own record, and it loses the stack trace doing so, because
|
|
|
|
|
`stacktracebuf` is the one thing that cannot be usefully summarized.
|
Document and test handing an error context between threads
The thread-safety section filed two different things under "does not cover,
and cannot": sharing a context between threads, and passing one to another
thread. Only the first is unsupported. Transfer already works by
construction -- the reference count is the only field the library reads
across an ownership boundary, and it is only ever touched under the pool
lock, so akerr_release_error() does not care which thread checked the slot
out. The pool is process-global, not thread-local, so a context outlives the
thread that raised it.
Calling that unsupported told readers the worker/collector shape was off the
table, which either cost them the pattern or cost them the stack trace when
they rolled their own struct instead.
Split the bullet: transfer joins the covered list and gets its own section
with the rule, the worked pattern, and the four receiving-side hazards
(PREPARE_ERROR cannot adopt, CATCH assigns over the pointer, FINISH in a
void helper still parses its return, and an unhandled error now terminates
from the collector's thread). Sharing keeps the "cannot" bullet, narrowed to
what it actually is.
err_threads_handoff.c proves it: the existing thread tests all keep every
context on the thread that raised it, so the transfer path was exercised
nowhere. Seven producers hand errors to one collector through a bounded
mutex/condvar queue -- the mutex is the thing under test, since it is what
publishes the unlocked content writes -- and the collector asserts the
context is still a live slot at refcount 1, that message and trace arrive
whole and in each producer's order, that the slot was never recycled in
flight, and that a thread which never called akerr_next_error() can release
it. A second phase reads a context whose raising thread has already exited.
Also document why copying a context by assignment is silently wrong:
stacktracebufptr is self-referential, so the copy's cursor points into the
source's buffer and the first append corrupts a slot the copier no longer
owns. TODO.md records the akerr_copy_error() shape that would fix it and the
trigger for building it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:35:18 -04:00
|
|
|
|
|
|
|
|
Copying the struct is not a workaround. `stacktracebufptr` is self-referential
|
|
|
|
|
(`include/akerror.tmpl.h`), so `akerr_ErrorContext c = *src;` leaves the copy's
|
|
|
|
|
cursor pointing into the source's buffer -- the copy logs correctly and then
|
|
|
|
|
corrupts a slot it does not own the first time anything appends to it. `arrayid`
|
|
|
|
|
is restored after the wipe in `akerr_release_error()` (`src/error.c:395-398`), so
|
|
|
|
|
a copied id makes the destination impersonate the source's slot for the life of
|
|
|
|
|
the process.
|
|
|
|
|
|
|
|
|
|
If this is ever worth an API, the shape is:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_copy_error(akerr_ErrorContext *source,
|
|
|
|
|
akerr_ErrorContext *destination);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
taking the destination as a parameter rather than allocating it. An allocating
|
|
|
|
|
copy could fail on pool exhaustion, and reporting *that* failure needs a pool
|
|
|
|
|
slot, so it would have to abort -- adding a third `exit()` site to a library that
|
|
|
|
|
deliberately has two. Caller-allocates puts the pool pressure where it can be
|
|
|
|
|
managed. The copy must repair four fields: `arrayid` (the destination's own),
|
|
|
|
|
`refcount` (set to 1, never inherited), `handled` (false -- a copy is a fresh
|
|
|
|
|
obligation, or `FINISH_NORETURN` on the receiving side drops it silently), and
|
|
|
|
|
`stacktracebufptr` (re-anchored to the destination's buffer at the *same offset*,
|
|
|
|
|
so a later append continues the trace instead of overwriting it).
|
|
|
|
|
|
|
|
|
|
Not worth building yet: no consumer needs it. The trigger is a consumer that
|
|
|
|
|
needs a worker's stack *trace*, not just its status and message, at the join
|
|
|
|
|
point -- `libakstdlib`'s planned `pthread_*` wrappers are the likely first.
|
|
|
|
|
|
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
|
|
|
## Unrelated pre-existing issues
|
|
|
|
|
|
|
|
|
|
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
|
|
|
|
and `NULL` are used unconditionally but only included under the stdlib branch.
|
Split the README reference material into docs/
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>
2026-08-01 16:56:07 -04:00
|
|
|
`docs/building.md`'s dependency list states what a replacement must provide,
|
|
|
|
|
but the header still needs its includes untangled for that configuration to
|
|
|
|
|
work.
|
2026-07-30 18:19:53 -04:00
|
|
|
- `CMakeLists.txt` sets `main_lib_dest` from `MY_LIBRARY_VERSION`, which is never
|
|
|
|
|
defined and never read. Dead line.
|