Record what the first full consumer had to work around
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m48s
libakstdlib CI Build / coverage (push) Successful in 2m37s
libakstdlib CI Build / mutation_test (push) Successful in 10m9s

akbasic is a ~6,300-line C interpreter built on this library. It is the first
consumer to exercise the whole surface rather than a corner of it, so what it
could not use is worth writing down: it prioritizes the section 3 wishlist by
what a real port actually reaches for, and it says which section 2 entries have
teeth.

The number that matters: across src/, akbasic makes 10 calls into this library
and 116 to raw libc. A library whose value proposition is turning silent libc
failures into error contexts is being bypassed 92% of the time by the consumer
most committed to it.

Four things it had to write for itself, each of which maps onto an existing
unchecked box. A strict strtoll/strtod wrapper, because 2.1.5 means aksl_atoi
would have turned four diagnosable errors into wrong answers -- VAL("garbage")
answering 0.0 instead of raising. A fixed-capacity string-keyed hash table,
which is 3.6's "hash map built on aksl_strhash_djb2", needed three times over
for variables, functions and labels. The bounded-copy-with-truncation-as-error
idiom at ten sites across six files. And case folding at three sites.

Also confirms 2.2.4 (aksl_sprintf unbounded -- akbasic deliberately never calls
it, and has 28 snprintf sites with no aksl_snprintf to route them through),
2.2.2 (aksl_fopen validation, reached from user input via DLOAD/DSAVE), 2.2.6
(the djb2 sign extension, benign here only because BASIC identifiers are ASCII)
and 2.1.4 (the missing va_end, which akbasic's text sink runs on every line of
program output).

Section 4.3 records what akbasic did *not* need, so the wishlist does not get
reordered purely by one consumer's shape: it allocates nothing and uses no lists
or trees, so a consumer that does allocate would weight 3.1's memory section far
higher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 06:29:01 -04:00
parent 95e5002512
commit fd71bcc67b

89
TODO.md
View File

@@ -624,3 +624,92 @@ Not libc wrappers, but the existing list/tree API is visibly incomplete:
NUL-terminated `aksl_strhash_djb2_str` convenience form. NUL-terminated `aksl_strhash_djb2_str` convenience form.
- [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers - [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers
pleasant to use. pleasant to use.
## 4. 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 is the first consumer to exercise the whole surface rather than a
corner of it, so what it *could not* use is worth recording: it prioritizes the wishlist in §3
by what a real port actually reaches for, and it confirms which §2 entries have teeth.
**The headline number.** Across `src/`, akbasic makes **10 calls into this library** and
**116 calls to raw libc**:
| `aksl_*` used | Count |
|---|---|
| `aksl_fopen` / `aksl_fclose` | 6 |
| `aksl_fprintf` | 2 |
| `aksl_fwrite` | 1 |
| `aksl_strhash_djb2` | 1 |
| Raw libc it had to use instead | Count | Wanted from §3 |
|---|---|---|
| `snprintf` | 28 | §3.1 bounded string formatting |
| `strlen` | 37 | §3.1 strings |
| `strcmp` | 16 | §3.1 strings |
| `strncpy` | 15 | §3.1 strings |
| `memcpy` / `memset` | 16 | wrapped, but see §2.2.11 |
| `strtoll` / `strtod` | 2 | §3.1 string → number |
| `fgets` | 2 | §3.1 stream I/O |
| `strstr` | 1 | §3.1 strings |
A library whose value proposition is "turn silent libc failures into error contexts" is being
bypassed 92% of the time by the consumer most committed to it. Every one of those calls is a
place where a failure goes unreported.
### 4.1 What the port had to write for itself
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). This is §2.1.5
biting, and it is not cosmetic: the Go reference this port reproduces checks `strconv`'s
error at four sites and turns each into a BASIC error. Routing them through `aksl_atoi`
would have converted four *diagnosable errors* into *wrong answers*`VAL("garbage")`
silently returning `0.0` rather than raising, and a malformed line number becoming line 0.
akbasic's own `TODO.md` §1.9 formally bans the `aksl_ato*` family for this reason. **The
fix wanted is exactly §3.1's `strtol` family plus the endptr/`errno`/range contract §2.1.5
describes**; when it lands, `src/convert.c` gets deleted.
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130 lines). §3.6
already calls for a "hash map built on `aksl_strhash_djb2`". This is that, three times over:
the interpreter needs one for variables, one for functions and one for labels per scope. It
is open-addressed with linear probing over a caller-supplied slot count, refuses rather than
resizes when full, and is the single most obviously-missing data structure in the library.
Worth lifting more or less verbatim if you want a starting point.
3. **The bounded-copy-with-truncation-as-error idiom, ten times.** `strncpy` followed by an
explicit NUL and a preceding length check appears at ten sites across six files. §3.1 lists
`strncpy` "with truncation reported as an error rather than silently accepted, which is the
whole value proposition here" — agreed, and the repetition is the evidence.
4. **Uppercase folding for case-insensitive lookup, three times.** Verb and function names are
case-insensitive in BASIC while variable names are not, so the port folds case before every
keyword lookup: by hand at `src/environment.c:197` and `src/parser_commands.c:113`, and
through `<ctype.h>`'s `toupper` in `src/verbs.c`. §3.1's `strcasecmp`/`strncasecmp` would
remove all three.
### 4.2 Confirmed with impact
- **§2.2.4 (`aksl_sprintf` is unbounded)** — akbasic never calls it, deliberately. Its 28
`snprintf` calls all write into fixed buffers whose size is the whole point, and there is no
`aksl_snprintf` to route them through. This is the single highest-value item in §3.1 for a
consumer of this shape.
- **§2.2.2 (`aksl_fopen` does not validate `pathname`/`mode`)** — real, and reached from user
input. akbasic's `DLOAD`/`DSAVE` take a filename from a BASIC string expression, so an empty
or unevaluated one is reachable from a running program; `akbasic_cmd_dload` validates the
name itself before handing it over, with a comment pointing here.
- **§2.2.6 (`aksl_strhash_djb2` sign-extends high bytes)** — benign for akbasic, which only
ever hashes 7-bit ASCII identifiers, and noted in its §1.3 as a hazard for anyone who later
keys a table on a filename or a string literal. Still worth fixing; the wrong answer is
platform-dependent, which is the bad kind of benign.
- **§2.1.4 (`va_end` is never called)** — akbasic's stdio text sink is built on `aksl_fprintf`
and therefore runs this UB on every line of program output. Nothing has misbehaved on
x86-64 glibc, which is exactly what makes it worth fixing before something does.
### 4.3 Not needed, and why
Recorded so the wishlist does not get prioritized purely by this consumer's shape. akbasic
uses **no** allocator (§3.1 memory), **no** lists or trees (§3.6), and **no** `aksl_realpath`:
it draws everything from fixed pools by design, and the `ak*` no-dynamic-allocation rule means
`calloc`/`realloc` would go unused here even if they existed. A consumer that does allocate
would weight §3.1's memory section far higher. The list and tree defects in §2.1.13 were
avoided rather than survived — akbasic's symbol tables are open-addressed precisely because
`aksl_list_append` truncates.