Files
libakstdlib/TODO.md
Tachikoma 91a8896b18
Some checks failed
libakstdlib CI Build / sanitizers (push) Has been cancelled
libakstdlib CI Build / coverage (push) Has been cancelled
libakstdlib CI Build / mutation_test (push) Has been cancelled
libakstdlib CI Build / cmake_build (push) Has been cancelled
Correct the error-pool entry: the pin is 1.0.0, upstream locks it
The largest item in the file said the error pool has no synchronisation of any
kind and that libakstdlib is therefore permanently single-threaded pending a
decision upstream. That is true of deps/libakerror as pinned -- 1.0.0, zero
occurrences of akerr_mutex_lock, no lock.h, no thread tests -- and false of
libakerror 2.0.1, which holds one recursive lock over the pool and the status
registry and ships docs/thread-safety.md.

libakgl and akbasic both pin 2.0.1 already.

Issue #2 is retitled and rescoped to the submodule bump and the concurrent test
that has to prove it, rather than a wait on somebody else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 18:57:39 -04:00

8.5 KiB

Record

Outstanding work is in the issue tracker, not in this file: https://source.starfort.tech/andrew/libakstdlib/issues

What stays here is what a tracker has no place for: where the library stands, and the decisions that would otherwise be re-litigated — what is deliberately not wrapped, and which uncovered lines are uncoverable rather than untested.

Issues are labelled by kind and blast radius, and milestoned by what they can land in: 0.2.x for anything that breaks no ABI, 0.3.0 for new public symbols, 1.0.0 for the large surfaces. Everything filed carries status::grooming until it has been through grooming.

Where the library stands

Wrapped 154 public functions across src/stdlib.c, src/string.c, src/stream.c, src/collections.c
Tests 17 CTest binaries plus 2 negative-compile entries, green under the default and sanitizer builds
Line coverage 99.5% (1708/1716)
Function coverage 100% (154/154)
Doxygen 100% of 154, gated — cmake --build build --target docs fails on an undocumented function, parameter or return
Mutation score 72.3% (188/260 sampled from 1701), gated at 65

The six confirmed defects that used to head this file are fixed and AKSL_KNOWN_FAILING_TESTS is empty. What they were, and what changed as a result, is in UPGRADING.md.

What libakerror costs this library

Three of these are not fixable from inside this repository, and each costs something here. They are filed in both places, because the fix is there and the bill is here. The fourth turned out not to be blocked at all:

Here Upstream What it costs
#2 Corrected while filing. The unlocked error pool is a property of the libakerror this repository pins (1.0.0), not of libakerror (2.0.1, which locks it). libakgl and akbasic are both on 2.0.1. The work is a submodule bump and the verification that goes with it, not a wait
#3 libakerror IGNORE() leaks a context, so aksl_tree_iterate open-codes log-then-release in four lines that should be one
#4 libakerror The coverage target is not namespaced when embedded, so -DAKSL_COVERAGE=ON fails to configure and CMakeLists.txt shadows add_custom_target to work around it
#5 libakerror No akerrorConfigVersion.cmake, so find_dependency(akerror) cannot ask for the 1.0.0 floor

Uncovered lines that are uncoverable

Eight lines, and this is what they are, so the coverage listing does not read as an oversight.

Two are the short-transfer branch in aksl_fread/aksl_fwrite — a short transfer with neither EOF nor a stream error. The standard permits it, so the branch is correct to have; every way of actually producing one on Linux sets feof or ferror first. It is the only error path in the library that has never executed, and reaching it needs a FILE * over a custom stream (fopencookie, funopen). That is #6.

Two are the string-buffer overflow guard — the capacity = needed arm in strbuf_reserve. Reaching it needs an aksl_StrBuf within a factor of two of SIZE_MAX, which is not a test, it is a hang. It is there because doubling a capacity is a multiplication, and an unguarded one is how a growable buffer turns into a heap overflow. Nothing to do.

Four are HANDLE(e, AKERR_ITERATOR_BREAK) lines, and they are macro artifacts rather than gaps. In libakerror that macro begins with the break; belonging to PROCESS's case 0: arm, reachable only when a callback returns a non-NULL context whose status is zero — the pathological case the errno-fallback work removed. Left uncovered deliberately rather than pinned by a test that would have to manufacture it.

aksl_version_check() ignores its patch argument

src/stdlib.c, the (void)patch. Correct for the current "same soname" rule — patch level never breaks the ABI — but the parameter exists only so the error message can name the caller's full version.

No consequence today. If a future compatibility rule needs the patch level to participate, that is the line to change, and the #if in tests/test_version.c is the test that encodes the rule.

Deliberate omissions

Recorded so nobody adds them thinking they were forgotten. Each is a decision, and each can be revisited with an argument.

Not wrapped Why
sprintf, vsprintf Cannot be bounded. An error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. aksl_snprintf and aksl_asprintf cover both real uses.
strtok Keeps its state in a hidden static, so two interleaved tokenisations corrupt each other silently and any use from a thread is a bug. aksl_strtok_r and aksl_strsep cover it.
strcpy, strcat as libc spells them Cannot be called safely without the destination's size. The wrappers take it.
setbuf Exactly setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ) and strictly less expressive. Wrapping it would add a second way to say one thing.
perror Writes to stderr and consults a global. aksl_strerror is the akerror-native equivalent and knows this library's own statuses as well as errno's.
strerror_r Two incompatible functions share that name and which one you get depends on feature-test macros a consumer cannot influence from inside this header. aksl_strerror is built on libakerror's registry instead.

What the mutation survivors mean

The harness samples 260 of 1701 mutants and kills 72.3%. Most of the 72 survivors are equivalent mutants rather than missing testsREADME.md has the full breakdown — and knowing which is which is the point, because a ratchet built on the wrong number is a ratchet that stops moving.

Three clusters are real work and are #7. Two survivors in that class were real and are fixed: the right child's depth + 1 in the depth-first walk, and aksl_tree_remove on an empty tree.

Evidence from the first full consumer

akbasic (source.starfort.tech/andrew/akbasic) is a ~6,300-line C interpreter built on this library and libakerror. It was the first consumer to exercise the whole surface rather than a corner of it, and what it could not use is what prioritised everything that has been built since.

The number that started it. Across src/, akbasic made 10 calls into this library and 116 to raw libc — a library whose value proposition is "turn silent libc failures into error contexts", bypassed 92% of the time by the consumer most committed to it.

Raw libc it had to use Count Now available as
strlen 37 aksl_strlen
snprintf 28 aksl_snprintf
strcmp 16 aksl_strcmp
memcpy / memset 16 aksl_memcpy / aksl_memset
strncpy 15 aksl_strncpy
strtoll / strtod 2 aksl_strtoll / aksl_strtod
fgets 2 aksl_fgets
strstr 1 aksl_strstr

All four things the port had to write for itself now exist here.

  1. A strict strtoll/strtod wrapper (akbasic/src/convert.c, ~60 lines). The aksl_strto* family is that, with the endptr/errno/range contract. akbasic formally banned the aksl_ato* family because routing four diagnosable errors through it would have turned them into wrong answers — VAL("garbage") silently returning 0.0. That ban can be lifted: the ato* forms report failures now.
  2. A fixed-capacity string-keyed hash table (akbasic/src/symtab.c, ~130 lines, needed three times over). aksl_hashmap_* is that table generalised, with tombstones on delete, which the original did not have.
  3. The bounded-copy-with-truncation-as-error idiom, at ten sites. aksl_strcpy and aksl_strncpy are exactly that idiom.
  4. Uppercase folding for case-insensitive lookup, three times. aksl_strcasecmp and aksl_strncasecmp.

And the four confirmed-with-impact defects are closed. The unbounded aksl_sprintf is gone; aksl_fopen's arguments are checked, so akbasic_cmd_dload's hand-rolled validation and its comment pointing here can go; the sign-extended djb2 reads bytes unsigned; and the missing va_end — which akbasic's stdio text sink ran on every line of program output — is fixed.

Still true, and still shaping the wishlist. akbasic uses no allocator, no lists and no trees, drawing everything from fixed pools by design. A consumer that does allocate would weight the open/read/write work far higher than this one does, so one consumer's count is evidence, not a plan. Re-counting against this release is #26.