diff --git a/TODO.md b/TODO.md index c421196..770c10d 100644 --- a/TODO.md +++ b/TODO.md @@ -624,3 +624,92 @@ Not libc wrappers, but the existing list/tree API is visibly incomplete: NUL-terminated `aksl_strhash_djb2_str` convenience form. - [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers 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 ``'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.1–3 were +avoided rather than survived — akbasic's symbol tables are open-addressed precisely because +`aksl_list_append` truncates.