src/error.c now scores 81.2%: 238 of 293 mutants killed, 204 by a
failing test, 24 by failing to compile, and 10 by hanging the suite --
deleting akerr_mutex_init() or the akerr_initializing re-entry guard
deadlocks the first test, which is the right answer for a broken lock.
Lock deletions are the one survivor category where surviving does not
mean harmless, so measure it rather than assume: rebuilt, the surviving
"delete the pool lock" mutant fails tests/err_threads_pool.c in 4 of 10
runs and fails under scripts/thread_test.sh in 5 of 5, with no false
positive on the unmutated library. The property assertions alone are a
coin flip on a missing lock; the sanitizer run is what holds that line.
The harness builds mutants with default CMake options and so never sees
it -- TODO item 8.
Also warn that a sanitized test binary run by hand does not inherit the
halt_on_error CTest gives it, and will print a race and still exit 0.
That is how the 5-of-5 above first read as 2 of 5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
The status-code ownership notes described work that has landed. That record
belongs in the commit that made the change and in the comments around the code
it constrains, not in a file whose purpose is naming what is left.
Keeps the six open items and the two unrelated pre-existing issues, and
promotes the missing sanitizer run to the top: mutation testing found an
out-of-bounds probe whose failure mode was a silent BSS write, which no
assertion-based test was positioned to catch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Replace the consumer-sized status-name array with private sparse storage and accept arbitrary integer status values. Add explicit range reservations with overlap diagnostics, reserve the library's 0-255 compatibility band, and harden pointer and string boundary handling.
Update regression coverage and document the required migration for custom status-code consumers.