Files
libakerror/TODO.md
Andrew Kesterson 64da04e83b
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / coverage (push) Successful in 2m46s
libakerror CI Build / mutation_test (push) Successful in 14m56s
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

4.1 KiB

TODO

Working notes for libakerror. Outstanding items only.

1. The test suite has no sanitizer run

Mutation testing caught an out-of-bounds probe in the status-name hash table that the suite could not: the failure mode was a write into adjacent BSS, which does not crash, so every test still passed. Sharpening one test closed that instance, but ASan would have caught the whole class directly and independently of how sharp the assertions are.

Add a -fsanitize=address,undefined build to .gitea/workflows/ci.yaml, or a CMake option alongside AKERR_COVERAGE. This is the highest-value item here: it covers the whole library, not just the registry, and the library's fixed pools and manual buffer arithmetic are exactly what it is good at.

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 "coordinate ranges at the dependency-stack level" advice in UPGRADING.md therefore has no tooling behind it.

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 dlopens 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.

5. The registry is not thread safe

Global mutable state, no locking, and akerr_status_name_count++ is not atomic. Currently documented as an initialization-time-only API rather than enforced. If components start initializing on separate threads this needs either a lock or a documented once-per-process init barrier.

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.

7. The library's own startup failure path is untested

__akerr_name_library_status() and the band reservation in akerr_init() log and then terminate when the library cannot name its own codes. Nothing exercises that: it needs a build whose AKERR_STATUS_NAME_SLOTS is too small to hold the library's own entries, and that macro is PRIVATE to the library target, so a test executable cannot set it. Mutation testing reports those lines as surviving mutants for this reason.

Closing it means a second library target built with a tiny table plus a WILL_FAIL test linked against it — worth doing when the CMake gains a sanitizer variant (item 1), since that adds the same machinery.

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. The README's dependency list states what a replacement must provide, but the header still needs its includes untangled for that configuration to work.
  • CMakeLists.txt sets main_lib_dest from MY_LIBRARY_VERSION, which is never defined and never read. Dead line.