|
|
|
|
@@ -1,11 +1,16 @@
|
|
|
|
|
# TODO
|
|
|
|
|
# Record
|
|
|
|
|
|
|
|
|
|
Working notes for `libakstdlib`. **Outstanding items only** — anything fixed comes
|
|
|
|
|
out of this file and goes into `UPGRADING.md`, the tests, or a comment beside the
|
|
|
|
|
code, whichever is the right place to be reminded of it.
|
|
|
|
|
**Outstanding work is in the issue tracker, not in this file:**
|
|
|
|
|
<https://source.starfort.tech/andrew/libakstdlib/issues>
|
|
|
|
|
|
|
|
|
|
Ordered by blast radius: what blocks other work first, what is merely wrong
|
|
|
|
|
second, what is missing last.
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
@@ -19,168 +24,61 @@ second, what is missing last.
|
|
|
|
|
| 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`.
|
|
|
|
|
`AKSL_KNOWN_FAILING_TESTS` is empty. What they were, and what changed as a result,
|
|
|
|
|
is in `UPGRADING.md`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
## What libakerror costs this library
|
|
|
|
|
|
|
|
|
|
## 1. Blocked on libakerror
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
These are not fixable from inside this repository. Each one currently costs
|
|
|
|
|
something here, and the cost is what makes them worth carrying.
|
|
|
|
|
| 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 |
|
|
|
|
|
|
|
|
|
|
### 1.1 The error pool is a process-global array with no locking
|
|
|
|
|
## Uncovered lines that are uncoverable
|
|
|
|
|
|
|
|
|
|
**This is what makes the library single-threaded**, and it is the largest open
|
|
|
|
|
item by some distance.
|
|
|
|
|
Eight lines, and this is what they are, so the coverage listing does not read as an
|
|
|
|
|
oversight.
|
|
|
|
|
|
|
|
|
|
`AKERR_ARRAY_ERROR` is a fixed array in `deps/libakerror/src/error.c`, handed out
|
|
|
|
|
by `akerr_next_error()` with no synchronisation of any kind. Every entry point in
|
|
|
|
|
this library takes a slot from it on any failure path, so two threads raising
|
|
|
|
|
errors concurrently can be handed the same slot and will corrupt each other's
|
|
|
|
|
message, status and stack trace.
|
|
|
|
|
**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.
|
|
|
|
|
|
|
|
|
|
**Consequence.** `README.md` says plainly that the library is not thread-safe.
|
|
|
|
|
That is honest, and it is also a hard ceiling: §4's `pthread_*` and socket
|
|
|
|
|
wrappers cannot be written until this is resolved, because a threading API
|
|
|
|
|
nobody can call from a thread is not an API.
|
|
|
|
|
**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.
|
|
|
|
|
|
|
|
|
|
**What closing it would touch.** libakerror's pool — either a mutex around
|
|
|
|
|
`akerr_next_error`/`akerr_release_error`, or thread-local slot arrays, which
|
|
|
|
|
would suit the bounded-preallocation style better and cost nothing on the
|
|
|
|
|
single-threaded path. Then a TSan job in `.gitea/workflows/ci.yaml` and a
|
|
|
|
|
concurrent smoke test here, and the warning in `README.md` comes out.
|
|
|
|
|
**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.
|
|
|
|
|
|
|
|
|
|
### 1.2 `IGNORE()` logs a context and never releases it
|
|
|
|
|
## `aksl_version_check()` ignores its `patch` argument
|
|
|
|
|
|
|
|
|
|
`deps/libakerror/include/akerror.tmpl.h:308`. The macro assigns the context to
|
|
|
|
|
`__akerr_last_ignored`, logs it, and stops. The slot is never returned to the
|
|
|
|
|
pool, so every `IGNORE()` on a failing call leaks one — and after
|
|
|
|
|
`AKERR_MAX_ARRAY_ERROR` of them the pool is exhausted and `ENSURE_ERROR_READY`
|
|
|
|
|
calls `exit(1)`.
|
|
|
|
|
|
|
|
|
|
**Consequence here.** `aksl_tree_iterate`'s `CLEANUP` block (`src/stdlib.c`)
|
|
|
|
|
cannot use `IGNORE()` to drop a queue-drain failure and open-codes the
|
|
|
|
|
log-then-release by hand instead, with a comment saying why. It is four lines
|
|
|
|
|
that should be one.
|
|
|
|
|
|
|
|
|
|
**What closing it would touch.** One `RELEASE_ERROR` in the macro; then delete
|
|
|
|
|
the workaround here.
|
|
|
|
|
|
|
|
|
|
### 1.3 libakerror does not namespace its `coverage` target when embedded
|
|
|
|
|
|
|
|
|
|
`deps/libakerror/CMakeLists.txt:172` versus `:189` — it namespaces `mutation` and
|
|
|
|
|
not `coverage`.
|
|
|
|
|
|
|
|
|
|
**Consequence here.** A `-DAKSL_COVERAGE=ON` top-level build fails to configure
|
|
|
|
|
at all: *"another target with the same name already exists"*. `CMakeLists.txt`
|
|
|
|
|
shadows `add_custom_target` for the duration of the `add_subdirectory()` call and
|
|
|
|
|
renames the dependency's to `akerror_coverage`.
|
|
|
|
|
|
|
|
|
|
**What closing it would touch.** Apply the same
|
|
|
|
|
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test upstream that the
|
|
|
|
|
`mutation` target already has; then delete the shadow here, which sits directly
|
|
|
|
|
above the `add_test` shadow and shares its comment.
|
|
|
|
|
|
|
|
|
|
### 1.4 libakerror installs no `akerrorConfigVersion.cmake`
|
|
|
|
|
|
|
|
|
|
**Consequence here.** `cmake/akstdlib.cmake.in` has to call
|
|
|
|
|
`find_dependency(akerror)` with no version, because a request for one would be
|
|
|
|
|
refused for want of a version file no matter what is installed. The 1.0.0 floor
|
|
|
|
|
therefore rests on `akstdlib.pc`'s `Requires:` and the `#error` guard in
|
|
|
|
|
`akstdlib.h`, neither of which covers a `find_package` consumer.
|
|
|
|
|
|
|
|
|
|
**What closing it would touch.** One `write_basic_package_version_file()` call
|
|
|
|
|
upstream — libakstdlib already does this correctly and can be copied — then add
|
|
|
|
|
the `1.0.0` floor to the `find_dependency` here.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 2. Known gaps in what is already wrapped
|
|
|
|
|
|
|
|
|
|
### 2.1 A short transfer with neither EOF nor a stream error is untested
|
|
|
|
|
|
|
|
|
|
`src/stdlib.c`, the `FAIL_RETURN(e, AKERR_IO, "short read: ...")` in `aksl_fread`
|
|
|
|
|
and its counterpart in `aksl_fwrite`. Two of the eight uncovered lines in the
|
|
|
|
|
whole library.
|
|
|
|
|
|
|
|
|
|
The standard permits a short transfer with neither indicator set, so the branch
|
|
|
|
|
is correct to have. Every way of actually producing one on Linux sets `feof` or
|
|
|
|
|
`ferror` first, so nothing in the suite reaches it.
|
|
|
|
|
|
|
|
|
|
**Consequence.** Low: the code is a handful of lines and reviewed, but it is the
|
|
|
|
|
only error path in the library that has never executed.
|
|
|
|
|
|
|
|
|
|
**What closing it would touch.** A `FILE *` over a custom stream — `fopencookie`
|
|
|
|
|
on glibc, `funopen` on the BSDs — whose read function returns a short count
|
|
|
|
|
without setting either flag. That is a platform-specific test helper in
|
|
|
|
|
`tests/aksl_capture.h` guarded on the platform, which is why it has not been
|
|
|
|
|
written yet rather than an oversight.
|
|
|
|
|
|
|
|
|
|
### 2.2 The string-buffer overflow guard is untestable
|
|
|
|
|
|
|
|
|
|
`src/collections.c`, 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. The other two uncovered lines.
|
|
|
|
|
|
|
|
|
|
**Consequence.** None known. It is there because doubling a capacity is a
|
|
|
|
|
multiplication, and an unguarded one is how a growable buffer turns into a heap
|
|
|
|
|
overflow.
|
|
|
|
|
|
|
|
|
|
**What closing it would touch.** Nothing worth doing. Recorded so the coverage
|
|
|
|
|
listing does not read as an oversight.
|
|
|
|
|
|
|
|
|
|
### 2.3 `aksl_version_check()` ignores its `patch` argument
|
|
|
|
|
|
|
|
|
|
`src/stdlib.c`, the `(void)patch`. Correct for the current "same soname" rule —
|
|
|
|
|
`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.
|
|
|
|
|
|
|
|
|
|
**Consequence.** None 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.
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
### 2.4 Surviving mutants worth turning into assertions
|
|
|
|
|
## Deliberate omissions
|
|
|
|
|
|
|
|
|
|
The mutation harness samples 260 of 1701 mutants and kills 72.3% of them. Most of
|
|
|
|
|
the 72 survivors are equivalent mutants rather than missing tests — `README.md`
|
|
|
|
|
has the full breakdown — but three clusters are real work:
|
|
|
|
|
|
|
|
|
|
- **`FINISH(e, true)` → `FINISH(e, false)`, 3 survivors.** An error swallowed
|
|
|
|
|
instead of propagated out of an `ATTEMPT` block, and nothing notices. Each one
|
|
|
|
|
is a call whose failure path is exercised but whose *propagation* is not: the
|
|
|
|
|
test asserts the status the callee raised without checking it came from the
|
|
|
|
|
callee rather than being re-raised locally. `tests/test_pool.c`'s origin
|
|
|
|
|
assertions are the shape of the fix.
|
|
|
|
|
- **`SUCCEED_RETURN` deleted, 7 survivors.** The function falls off the end and
|
|
|
|
|
returns whatever is in the return register, which is NULL often enough to pass.
|
|
|
|
|
These need an assertion on the *side effect* — the buffer that was filled, the
|
|
|
|
|
node that was linked — rather than on the returned status.
|
|
|
|
|
- **`FAIL_*` guards deleted, ~6 of the 14 in that group.** Each is an argument
|
|
|
|
|
check nothing drives. The other 8 in the group are constant shifts that no test
|
|
|
|
|
can catch, because the test names the same constant symbolically and moves with
|
|
|
|
|
it.
|
|
|
|
|
|
|
|
|
|
Two survivors in this 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.
|
|
|
|
|
|
|
|
|
|
**What closing them would touch.** Only `tests/`. Raise the `--threshold` in
|
|
|
|
|
`.gitea/workflows/ci.yaml` and `.githooks/pre-push` in step, as a ratchet.
|
|
|
|
|
|
|
|
|
|
### 2.5 Four uncovered `HANDLE(e, AKERR_ITERATOR_BREAK)` lines
|
|
|
|
|
|
|
|
|
|
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* — which is the pathological case
|
|
|
|
|
the errno-fallback work removed. Left uncovered deliberately rather than pinned
|
|
|
|
|
by a test that would have to manufacture it.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 3. Deliberate omissions
|
|
|
|
|
|
|
|
|
|
Recorded so nobody adds them thinking they were forgotten. Each is a decision,
|
|
|
|
|
and each can be revisited with an argument.
|
|
|
|
|
Recorded so nobody adds them thinking they were forgotten. **Each is a decision,
|
|
|
|
|
and each can be revisited with an argument.**
|
|
|
|
|
|
|
|
|
|
| Not wrapped | Why |
|
|
|
|
|
|---|---|
|
|
|
|
|
@@ -191,126 +89,23 @@ and each can be revisited with an argument.
|
|
|
|
|
| `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
|
|
|
|
|
|
|
|
|
|
## 4. Not yet wrapped
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
Ordered by how much a caller of this library would miss them. §3.1 and §3.6 of
|
|
|
|
|
the old numbering are done; what follows is what is left.
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
### 4.1 POSIX file and process API
|
|
|
|
|
|
|
|
|
|
The most likely next surface. Nothing here is blocked; it is simply not written.
|
|
|
|
|
|
|
|
|
|
**`unistd.h` / `fcntl.h`**
|
|
|
|
|
- [ ] `open`, `close`, `read`, `write`, `pread`, `pwrite`, `lseek`
|
|
|
|
|
- [ ] `readv`, `writev`
|
|
|
|
|
- [ ] `dup`, `dup2`, `pipe`, `fcntl`
|
|
|
|
|
- [ ] `fsync`, `fdatasync`, `truncate`, `ftruncate`
|
|
|
|
|
- [ ] `unlink`, `link`, `symlink`, `readlink`, `rmdir`, `mkdir`
|
|
|
|
|
- [ ] `access`, `faccessat`, `chmod`, `fchmod`, `chown`, `fchown`, `umask`
|
|
|
|
|
- [ ] `chdir`, `fchdir`, `getcwd`
|
|
|
|
|
- [ ] `isatty`, `ttyname_r`
|
|
|
|
|
- [ ] `sysconf`, `pathconf`
|
|
|
|
|
- [ ] `sleep`, `usleep`, `nanosleep`
|
|
|
|
|
|
|
|
|
|
The short-read/short-write contract is the interesting part: `read(2)` returning
|
|
|
|
|
fewer bytes than asked for is *normal* on a pipe or a socket and a failure on a
|
|
|
|
|
regular file, so the wrapper needs the same transferred-count out-param
|
|
|
|
|
`aksl_fread` has, and callers need to be told which case they are in.
|
|
|
|
|
|
|
|
|
|
**`sys/stat.h`**
|
|
|
|
|
- [ ] `stat`, `fstat`, `lstat`, `fstatat`
|
|
|
|
|
- [ ] `statvfs`, `fstatvfs`
|
|
|
|
|
|
|
|
|
|
**`dirent.h`**
|
|
|
|
|
- [ ] `opendir`, `fdopendir`, `readdir`, `closedir`, `rewinddir`, `scandir`
|
|
|
|
|
|
|
|
|
|
`readdir(3)` returning NULL for both "end of directory" and "error, check errno"
|
|
|
|
|
is the same conflation `aksl_fgetc` and `aksl_fgets` already untangle, and should
|
|
|
|
|
be untangled the same way: AKERR_EOF for the end, the errno for the error.
|
|
|
|
|
|
|
|
|
|
**Process control**
|
|
|
|
|
- [ ] `fork`, the `exec*` family, `waitpid`, `wait`
|
|
|
|
|
- [ ] `posix_spawn`
|
|
|
|
|
- [ ] `system`, `popen`, `pclose`
|
|
|
|
|
- [ ] `getpid`, `getppid`, `getuid`, `geteuid`, `setuid`, `setgid`
|
|
|
|
|
- [ ] `atexit`, `exit`, `_exit`, `abort` — mostly to give akerror a shutdown hook
|
|
|
|
|
- [ ] `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv`
|
|
|
|
|
|
|
|
|
|
`fork` needs thinking about before it is wrapped: the error pool is inherited by
|
|
|
|
|
the child, and any context live at the moment of the fork exists twice
|
|
|
|
|
afterwards.
|
|
|
|
|
|
|
|
|
|
**`sys/mman.h`**
|
|
|
|
|
- [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise`
|
|
|
|
|
|
|
|
|
|
### 4.2 Time
|
|
|
|
|
|
|
|
|
|
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
|
|
|
|
|
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
|
|
|
|
|
- [ ] `strftime`, `strptime`
|
|
|
|
|
- [ ] `clock`, `times`
|
|
|
|
|
|
|
|
|
|
`strftime(3)` returns 0 for both "the output was empty" and "it did not fit",
|
|
|
|
|
which is the bounded-write ambiguity `aksl_snprintf` already resolves.
|
|
|
|
|
|
|
|
|
|
### 4.3 Sorting, searching and the rest of `stdlib.h`
|
|
|
|
|
|
|
|
|
|
- [ ] `qsort`, `qsort_r`, `bsearch` — a comparator that fails currently has
|
|
|
|
|
nowhere to put the error. An akerror-aware comparator signature, shaped
|
|
|
|
|
like the `aksl_TreeCompareFunc` the tree functions already take, would be a
|
|
|
|
|
genuine improvement over libc rather than a wrapper around it.
|
|
|
|
|
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv` — note that
|
|
|
|
|
`abs(INT_MIN)` is undefined behaviour, which is exactly the kind of silent
|
|
|
|
|
trap worth surfacing.
|
|
|
|
|
- [ ] `rand`, `srand`, `random`, `srandom`, `getrandom`/`arc4random`
|
|
|
|
|
|
|
|
|
|
### 4.4 Larger surfaces
|
|
|
|
|
|
|
|
|
|
- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, the
|
|
|
|
|
`send`/`recv` families, `shutdown`, `setsockopt`/`getsockopt`,
|
|
|
|
|
`getaddrinfo`/`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`.
|
|
|
|
|
`getaddrinfo` is the interesting one: it has its own error space, neither
|
|
|
|
|
errno nor akerror, which needs mapping into a registered status range.
|
|
|
|
|
- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_*`
|
|
|
|
|
- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`,
|
|
|
|
|
`raise`, `signalfd`. A handler cannot raise an akerror context — the pool is
|
|
|
|
|
not async-signal-safe — so the wrapper covers installation and masking only,
|
|
|
|
|
and that limit should be documented rather than discovered.
|
|
|
|
|
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`,
|
|
|
|
|
`pthread_cond_*`, `pthread_rwlock_*`, `sem_*`. **Blocked on §1.1.**
|
|
|
|
|
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror`. `dlerror(3)`
|
|
|
|
|
is the same "returns a string and clears itself" trap as `strerror`.
|
|
|
|
|
- [ ] **Locale / wide chars**: `setlocale`, `mbstowcs`, `wcstombs`, `iconv_*`
|
|
|
|
|
- [ ] **Math**: the `math.h` functions that set `errno` or raise FP exceptions
|
|
|
|
|
(`sqrt`, `log`, `pow`, `acos`, …). Probably a dedicated `libakmath` rather
|
|
|
|
|
than more surface here — the failure model is `fetestexcept`, not errno, and
|
|
|
|
|
it does not resemble anything else in this library.
|
|
|
|
|
|
|
|
|
|
### 4.5 Data structures
|
|
|
|
|
|
|
|
|
|
The list and tree API is complete against what §3.6 asked for. What a second
|
|
|
|
|
consumer might want next:
|
|
|
|
|
|
|
|
|
|
- [ ] A dynamic array / vector, on the same "caller owns the storage" terms as
|
|
|
|
|
`aksl_HashMap`.
|
|
|
|
|
- [ ] A hash map keyed on something other than a string — the current one copies
|
|
|
|
|
keys into fixed slots, which is right for identifiers and wrong for
|
|
|
|
|
anything large or binary.
|
|
|
|
|
- [ ] Balanced insertion for the tree. It is a plain unbalanced BST and says so;
|
|
|
|
|
sorted input gives a degenerate chain that `AKSL_TREE_MAX_DEPTH` then
|
|
|
|
|
refuses. Bounded rather than dangerous, but a red-black or AVL variant is
|
|
|
|
|
the honest fix if anyone inserts sorted data in earnest.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 5. Evidence from the first full consumer
|
|
|
|
|
## 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 the work above.
|
|
|
|
|
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
|
|
|
|
|
@@ -330,28 +125,27 @@ committed to it.
|
|
|
|
|
|
|
|
|
|
**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's own `TODO.md` §1.9 formally bans 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. **`src/convert.c` can be deleted.**
|
|
|
|
|
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`.
|
|
|
|
|
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 items are closed.** §2.2.4's unbounded
|
|
|
|
|
`aksl_sprintf` is gone; §2.2.2's unchecked `aksl_fopen` arguments are checked, so
|
|
|
|
|
**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;
|
|
|
|
|
§2.2.6's sign-extended djb2 reads bytes unsigned; and §2.1.4's missing `va_end` —
|
|
|
|
|
which akbasic's stdio text sink ran on every line of program output — is fixed.
|
|
|
|
|
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 §4.1's `open`/`read`/`write` far higher than this one
|
|
|
|
|
does. The next thing worth doing is porting akbasic onto this release and
|
|
|
|
|
counting the calls again.
|
|
|
|
|
**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.
|
|
|
|
|
|