Twenty-six issues on source.starfort.tech/andrew/libakstdlib, 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. The four items blocked on libakerror are filed in both places -- the fix is there and the bill is here -- and TODO.md keeps a table saying which is which. What stays in TODO.md is what a tracker has no place for: where the library stands, why six libc functions are deliberately not wrapped, which eight uncovered lines are uncoverable rather than untested, and the akbasic call counts that prioritised everything built since. 377 lines to 189. Filed alongside: 25 file headers in tests/ and src/ cite TODO.md section numbers from a numbering the file itself had already abandoned. They label completed work, so nothing about the code is misleading -- only the pointer is. Left for its own commit rather than churning 25 files here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
152 lines
8.3 KiB
Markdown
152 lines
8.3 KiB
Markdown
# 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`.
|
|
|
|
## The four items blocked upstream
|
|
|
|
Not fixable from inside this repository, and each currently costs something here.
|
|
Filed both here and against libakerror, because the fix is there and the bill is
|
|
here:
|
|
|
|
| Here | Upstream | What it costs |
|
|
|---|---|---|
|
|
| #2 | libakerror | The unlocked error pool, **which is what makes this library single-threaded**. The threading and socket wrappers cannot be written until it is resolved |
|
|
| #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 tests** — `README.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.
|