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>
152 lines
8.5 KiB
Markdown
152 lines
8.5 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`.
|
|
|
|
## 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 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.
|