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>
44 KiB
TODO
Working notes for libakstdlib — the akerror-wrapped libc surface.
Scope of the current library (include/akstdlib.h, src/stdlib.c): 16 libc wrappers
(fopen, fread, fwrite, fclose, malloc, free, memset, memcpy, printf,
fprintf, sprintf, atoi, atol, atoll, atof, realpath), one hash helper
(aksl_strhash_djb2), a doubly-linked list (append/pop/iterate) and a binary tree
iterator (aksl_tree_iterate).
Items marked [CONFIRMED] were reproduced by building the library and running a probe program against it, not just read off the source.
1. Unit tests that should be written
1.0 Test-harness prerequisites — DONE (branch feature/test-harness)
ctest is green: 5/5, no Not Run entries. Everything in §1.1–1.9 can now be written
against tests/aksl_capture.h and added to the AKSL_TESTS list.
-
The current tests cannot fail.
tests/test_linkedlist.casserted nothing at all; it printed node names and returned0unconditionally, so any of the list bugs in §2.1 would pass it. Rewritten as 15 assertion-based cases covering the append, iterate and pop behaviour that is correct today. -
tests/test_tree.cwas red.ctestreportedtree (Failed), exit 136, becauseparms.stepswas never reset between the three searches, so the second assertion compared an accumulated14against7. Each search now builds its own tree and params. The file records that its step counts do not actually distinguish the three traversal orders — real order assertions remain §1.8. -
Adopt libakerror's test conventions.
tests/aksl_capture.hprovidesAKSL_CHECK()(anNDEBUG-proof assert),AKSL_CHECK_STATUS()/AKSL_CHECK_OK()which run an akerror-returning call, assert on its status and release the context,AKSL_CHECK_MSG_CONTAINS(), a capturingakerr_log_method,aksl_slots_in_use()for pool-leak checks, and anAKSL_RUN()driver that additionally fails any test which leaks an error-pool slot. -
One test source per behaviour, registered with CTest via a list variable.
tests/test_<name>.c→ executabletest_<name>→ CTest test<name>, driven byAKSL_TESTSinCMakeLists.txt. -
Add a
WILL_FAILcategory. Two lists:AKSL_WILL_FAIL_TESTSfor tests that abort by design (empty for now) andAKSL_KNOWN_FAILING_TESTSfor the confirmed defects in §2.1 — see the note at the head of §2.1. -
Fix the four permanently-failing submodule tests.
deps/libakerroris addedEXCLUDE_FROM_ALL, so akerror'sadd_testentries registered but its test binaries were never built, andctestreportederr_catch,err_cleanup,err_trace,err_improper_closureas Not Run → failed on every run. Neither the pinned commit nor upstreammainguards that registration, CMake has no way to un-register a test, andset_tests_propertiescannot reach across directory scopes — soadd_testandset_tests_propertiesare shadowed for the duration of theadd_subdirectorycall. The dependency has its own CI; this suite now contains only this project's tests. -
Wire in a memory-error build.
cmake -S . -B build-asan -DAKSL_SANITIZE=ONadds-fsanitize=address,undefined -fno-sanitize-recover=allto the library, the tests and the vendored libakerror. Currently clean, and it is the intended way to test the items in §2 that only misbehave under instrumentation (uninitialised%sinaksl_realpath, unboundedvsprintf, missingva_end). -
Port
scripts/mutation_test.pyfrom libakerror. Retargeted atsrc/stdlib.candinclude/akstdlib.h(178 mutants) and exposed ascmake --build build --target mutation. CI runs the narrowersrc/stdlib.cset (173 mutants) as its ownmutation_testjob with--threshold 80and a JUnit report. Current score: 89.6% — 155 killed, 18 survived. It was 46.8% (81 killed, 92 survived) before §1.2–1.6 were written; the remaining survivors are:| surviving mutants | where | why | |---|---|---| | 5 | deleted statements whose absence nothing observes (`free(ptr)`, `obj->next = NULL`, three `SUCCEED_RETURN`s) | needs an assertion on the side effect, not on the status | | 4 | `aksl_list_append`'s cycle/tail walk | the function is broken here (§2.1.1); its known-failing test cannot pin the internals yet | | 4 | `lalloc`/`lfree` defaulting in `aksl_tree_iterate` | dead parameters (§2.2.8) — defaulted, then never called | | 3 | `*count == -1` in the `printf` family | `*count` on the error path is itself a contract gap (§1.3) | | 2 | `feof` in `aksl_fwrite`, `break` after the BFS `FAIL_RETURN` | unreachable in practice | The threshold is a regression ratchet, not a quality bar; raise it as those survivors become assertions. Each one is a concrete missing test — `mutation-junit.xml` lists them with `file:line` and the exact edit. -
Cap every test with a CTest
TIMEOUT. Found while measuring the above: a mutant that deletesfast = fast->next->nextturnsaksl_list_iterateinto an infinite loop, soctestwaited forever, the mutation harness killedctestat its own 120 s timeout, and the orphaned test binary kept spinning at 100% CPU while holding the pipe open — the run wedged and had to be killed by hand.TIMEOUT 30on every test makesctestreap its own child, so those mutants are now killed in 30 s and leave nothing behind. It also guards the real suite against the same class of bug. -
CI could not have run any of this.
actions/checkout@v4was not fetching submodules, soadd_subdirectory(deps/libakerror)had nothing to descend into and the configure step failed before ever reaching the tests. Addedsubmodules: recursive, and swappedcmake --build build --target testforctest --output-on-failureso a failure is diagnosable from the job log. The deeper problem — CI installinglibakerror@mainwhile the build actually compiles the pinned submodule — is §2.3.
1.1 Memory wrappers
aksl_malloc— happy path returns non-NULL and writes throughdst.aksl_malloc(n, NULL)→AKERR_NULLPOINTER, message content asserted.aksl_malloc(0, &p)— pin down the contract. Today the result depends on whether the platform'smalloc(0)returns NULL; if it does, the wrapper reportserrno, which may be0. See §2.2.1.aksl_malloc(SIZE_MAX, &p)→ allocation failure surfacesENOMEM, and*dstis left NULL rather than garbage.aksl_free(NULL)→AKERR_NULLPOINTER(assert the documented behaviour, sincefree(NULL)is legal C and callers will be surprised).aksl_freehappy path, and a malloc→free round trip under ASan.aksl_memset— happy path fills the buffer;n == 0is a no-op;s == NULL→AKERR_NULLPOINTER.aksl_memcpy— happy path copies;d == NULLands == NULLeach →AKERR_NULLPOINTER;n == 0with valid pointers succeeds.aksl_memcpywith overlapping regions — decide and test whether this is rejected (AKERR_VALUE) or documented as UB likememcpy.
1.2 File I/O
Covered by tests/test_stream.c.
aksl_fopenhappy path on a temp file;*fpis written.aksl_fopen("/nonexistent/path", "r", &fp)→ENOENTpropagated as the status, with the pathname in the message.aksl_fopenon a mode-denied path (e.g./proc/1/mem, or achmod 000temp file) →EACCES.aksl_fopen(path, mode, NULL)→AKERR_NULLPOINTER.aksl_fopen(NULL, "r", &fp)andaksl_fopen(path, NULL, &fp)→ currently unchecked, see §2.2.2. Test once the guards exist.aksl_freadfull read; short read at EOF →AKERR_EOF; read from a write-only stream →AKERR_IO;fp == NULL→AKERR_NULLPOINTER.ptr == NULLis still unchecked and untested (see §2.2.3).aksl_freadpartial read that is neither EOF nor error — assert whatever the fixed contract is; today this silently returns success and the caller cannot tell how many members were read.aksl_fwritehappy path; write to a read-only stream →AKERR_IO;fp == NULL→AKERR_NULLPOINTER. The full-device (/dev/full) case is still open.aksl_fclosehappy path;NULL→AKERR_NULLPOINTER. Double-close is still undecided, so it is neither caught nor tested.aksl_fcloseon a stream whose buffered flush fails (/dev/full) → non-zerofclosesurfaceserrno.- Round-trip test:
fopen→fwrite→fclose→fopen→fread→ compare bytes.
1.3 Formatted output
Covered by tests/test_format.c.
aksl_printf/aksl_fprintf/aksl_sprintfhappy paths, asserting both the byte count written throughcountand the produced text. (aksl_printfis checked by pointingstdoutat a temp file for the duration of the call.)- Each of the three with every pointer argument NULL in turn →
AKERR_NULLPOINTER. aksl_fprintfto a read-only stream → theerrnoit saw (EBADF) with "Short write" in the message.*countis still left holding-1; the test asserts the status only, so it will not have to change when that is fixed.aksl_sprintfwith a format that overflows the destination — currently unbounded (§2.2.4). Test theaksl_snprintfreplacement once it exists.- Regression test for the missing
va_end(§2.1.4) — 512 variadic calls in a loop, which the sanitizer build also runs. - Format-string/arg mismatch is caught at compile time once
__attribute__((format(printf, ...)))is added (§2.2.5) — a negative compile test.
1.4 String → number
Happy paths and NULL guards are covered by tests/test_convert.c; the strict-conversion
contract is asserted by tests/test_convert_strict.c, which is registered as a known
failure until §2.1.5 is fixed.
aksl_atoi/atol/atoll/atofhappy paths, including negative values and leading whitespace.- NULL
nptrand NULLdestfor each →AKERR_NULLPOINTER. - Non-numeric input (
"not a number") — [CONFIRMED] currently returns success with*dest == 0.AKERR_VALUEis asserted by the known-failing test. - Overflow (
"99999999999999999999") — [CONFIRMED] currently returns success with a garbage value (-1on this box).ERANGEis asserted by the known-failing test. - Empty string,
" "and"12abc"(trailing junk) →AKERR_VALUE, in the known-failing test."0x10"and"inf"/"nan"foratofare still open. LONG_MIN/LONG_MAX/LLONG_MIN/LLONG_MAXboundary strings round-trip exactly.
1.5 aksl_realpath
Covered by tests/test_path.c. Every failure case there passes a zeroed resolved_path,
because the wrapper's own error path formats that buffer with %s (§2.1.6).
- Happy path on an existing file and on a symlink chain; result matches
realpath(3). - Non-existent path →
ENOENT. - A path component that is not a directory →
ENOTDIR. - Symlink loop →
ELOOP. path == NULL→AKERR_NULLPOINTER.resolved_path == NULL— currently unchecked and leaks (§2.1.6). Test once fixed.- A failure case where
resolved_pathis an uninitialised buffer — this is the crash case in §2.1.6; run it under ASan/MSan.
1.6 aksl_strhash_djb2
Covered by tests/test_strhash.c.
- Known-answer vectors:
djb2("")== 5381; a handful of fixed strings with their pre-computed 32-bit values. len == 0returns 5381 regardless ofstrcontents.- NULL
strand NULLhashval→AKERR_NULLPOINTER. - High-bit bytes (
"\xff\xfe") — pins down the sign-extension bug in §2.2.6; the expected value must be theunsigned charone (5868578; the wrapper returns the sign-extended 5859874 today). Every vector in the test file is 7-bit ASCII precisely so that it says nothing about this case either way. - Embedded NUL bytes are hashed (the function is length-driven, not NUL-driven).
- Same input → same output across two calls (no hidden state).
1.7 Linked list
aksl_list_appendbuilds a correct chain of N nodes — [CONFIRMED BROKEN], see §2.1.1. Appendingn1..n4ton0yields the chainn0 -> n4;n1,n2,n3are silently dropped. This is the single most important test to add.appendsetsobj->prevto the real tail andobj->nextto NULL.appendonto an empty (single, zeroed) node.appendNULL list / NULL obj →AKERR_NULLPOINTER.appendonto a list containing a cycle →AKERR_CIRCULAR_REFERENCE, for a self-loop, a 2-node cycle, and a cycle that does not include the head.appenda node that is already in the list (aliasing) — define and test the contract.aksl_list_iteratevisits every node exactly once, starting at the head — [CONFIRMED BROKEN], see §2.1.2: it starts iterating from the midpoint left behind by the cycle detector, so the head is never visited. Assert both the visit count and the visit order.iterateover a single-node list visits exactly one node.iterateNULL list / NULL iter →AKERR_NULLPOINTER.iterateover a cyclic list →AKERR_CIRCULAR_REFERENCE(self-loop, 2-node, tail-to-middle).iteratewhere the callback raisesAKERR_ITERATOR_BREAK→ iteration stops at that node, the wrapper returns success, and the visit count proves the early exit.iteratewhere the callback raises some other error → that error propagates out unchanged, with the callback's message intact.aksl_list_popon a middle node relinksprev/nextcorrectly and clears the popped node's pointers.popon the head node, on the tail node, and on a single-node list.pop(NULL)→AKERR_NULLPOINTER.poptheniterate— the list is still traversable and the popped node is gone.- Pool-accounting test: after a long sequence of list operations including failures,
akerr_slots_in_use() == 0.
1.8 Tree
- Visit order assertions, not just counts, for
DFS_PREORDER,DFS_INORDERandDFS_POSTORDERover the 7-node tree — record the visited node pointers into an array and compare against the expected sequence. The current test only counts steps, which cannot distinguish the three orders (all three visit 7 nodes). AKERR_ITERATOR_BREAKaborts the whole traversal — [CONFIRMED BROKEN], see §2.1.3. The pre-order case exists astests/test_tree_iterate_break.c: pre-order visits 0, 1, 3, so breaking attree[3]must stop the walk at 3 visits, and today it runs on to all 7. Add the equivalent for in-order (3, 1, 4, 0, 5, 2, 6 → breaking attree[4]gives 3) and post-order (3, 4, 1, 5, 6, 2, 0 → breaking attree[5]gives 4).AKSL_TREE_SEARCH_BFSandAKSL_TREE_SEARCH_BFS_RIGHT→AKERR_NOT_IMPLEMENTEDtoday, asserted intests/test_tree.calong with the callback never being called; replace with real order assertions once implemented (§3 / §2.2.9).aksl_tree_iterateNULLroot/ NULLiter→AKERR_NULLPOINTER, and a callback error that is notAKERR_ITERATOR_BREAKpropagates out to the caller.- Unknown
searchmode(e.g.99) — [CONFIRMED] currently returns success having visited nothing. Should beAKERR_VALUE. AKSL_TREE_SEARCH_VISIT(defined in the header,5) — [CONFIRMED] falls into the same silent-success hole. Either implement it or reject it.root == NULL/iter == NULL→AKERR_NULLPOINTER.- Single-node tree (no children) visits exactly once, in all three orders.
- Left-only and right-only degenerate chains.
- Callback raising a non-
ITERATOR_BREAKerror propagates out of the recursion with the original status and message. - Custom
lalloc/lfreeare actually invoked — currently they are stored and never called (§2.2.8), so this test will fail until BFS lands. - Deep/degenerate tree (e.g. 100k-node left chain) — documents the recursion-depth limit (§2.2.7).
- A tree containing a cycle (child pointing back at an ancestor) — currently infinite
recursion; test for
AKERR_CIRCULAR_REFERENCEonce guarded.
1.9 Cross-cutting
- Error-pool accounting: for every wrapper, a test that drives the failure path
AKERR_MAX_ARRAY_ERROR + 10times and asserts the pool does not leak (akerr_slots_in_use() == 0after each handled error). - Stack-trace content: assert that the file/function/line recorded by each wrapper's
FAILpoints atsrc/stdlib.cand the right function name. AKERR_NOIGNOREis effective: a negative compile test where a wrapper's return is discarded and-Werror=unused-resultfires.- Thread safety: libakerror's
AKERR_ARRAY_ERRORis a process-global array with no locking. Iflibakstdlibis meant to be callable from threads, add a concurrent smoke test under TSan — and if it is not, say so in the README.
2. Corner cases in existing functionality that should be resolved
2.1 Confirmed defects (reproduced against the built library)
Four of these now have a failing test apiece, registered in AKSL_KNOWN_FAILING_TESTS
and therefore marked WILL_FAIL so the suite stays green while the gap stays visible:
tests/test_list_append_chain.c, tests/test_list_iterate_head.c,
tests/test_tree_iterate_break.c and tests/test_convert_strict.c. When one of these
defects is fixed, CTest reports that test as failed with unexpectedly passed — that is the
cue to move it into AKSL_TESTS.
-
aksl_list_appenddoes not find the tail — it silently truncates the list.src/stdlib.c:194. The function conflates Floyd cycle detection with tail-finding:tailis set toslowbeforeslowadvances, so it tracks the node behind the midpoint, not the tail. Appendingn1,n2,n3,n4ton0produces the chainn0 -> n4;n1–n3are unlinked and lost. The fix is to keep the cycle check but walk a separate cursor to the real tail (while (tail->next) tail = tail->next;), or run Floyd first and then walk to the end. -
aksl_list_iterateskips the first half of the list.src/stdlib.c:324. After the cycle-detection loop,slowis left at the list midpoint, and the visiting loop then starts fromslowinstead of fromlist. The head node is never passed to the callback. Fix: iterate fromlist, not fromslow. -
AKERR_ITERATOR_BREAKdoes not stop a tree traversal.src/stdlib.c:251. The recursive frame in which the callback raises the break handles it in its ownPROCESS/HANDLE(e, AKERR_ITERATOR_BREAK)block and returns success; the parent'sPASStherefore sees no error and continues on to the sibling subtree. Breaking attree[3]in pre-order still visits all 7 nodes. Fix by splitting the recursion: an internal helper that propagatesAKERR_ITERATOR_BREAKunhandled, and a public entry point that swallows it exactly once at the top.tests/test_tree.ccurrently passes only because the search target is the last node in all three orders. -
va_endis never called.src/stdlib.c:96,:109,:122each callva_startwith no matchingva_endon any path — happy or error. This is undefined behaviour per the C standard and leaks register-save state on some ABIs. -
aksl_atoi/atol/atoll/atofcannot report a conversion failure.src/stdlib.c:135–169.atoi("not a number")returns success with0;atoi("99999999999999999999")returns success with a wrapped value. Since the entire point of this library is turning silent libc failures into error contexts, these should be reimplemented overstrtol/strtoll/strtodwitherrno = 0before the call, anendptrcheck for "no digits consumed" and "trailing junk", and a range check — raisingAKERR_VALUEandERANGErespectively. Keep theatoi-compatible names but document the stricter contract, or addaksl_strtol-family wrappers alongside.tests/test_convert_strict.casserts that contract and is registered as a known failure. -
aksl_realpathmishandlesresolved_path.src/stdlib.c:171.resolved_pathis never NULL-checked.realpath(path, NULL)is valid and mallocs a buffer, but the wrapper discardsresult, so the caller gets nothing and the buffer leaks.- On failure the message formats
resolved_pathwith%swhilerealpathleaves it unspecified — for the normal caller who passed an uninitialised stack buffer, the error path itself reads uninitialised memory and can crash. - There is no way to express the buffer's size, so callers must know to supply
PATH_MAXbytes. Consideraksl_realpath(const char *path, char *buf, size_t buflen), or an allocating variant that returns the malloc'd pointer through an out-param.
2.2 Latent issues and API contract gaps
-
errnois used as an error status where it may be stale.aksl_mallocreportserrnowhenmallocreturns NULL, butmalloc(0)may legitimately return NULL without settingerrno, producing aFAILwithstatus == 0— an error context that everyDETECT/CATCHwill read as success while still holding a pool slot. Guard everyerrno-sourced status with a fallback (errno ? errno : AKERR_IO) and seterrno = 0before the call being wrapped. -
aksl_fopendoes not validatepathnameormode.fopen(NULL, ...)is UB. AddAKERR_NULLPOINTERguards. -
aksl_fread/aksl_fwritelose the transfer count and hide short transfers.ptris never NULL-checked in either function.- Neither has an out-param for the number of members actually transferred, so a caller who
gets
AKERR_EOFcannot tell how much data arrived. Addsize_t *nmemb_out. - If
nmemr != nmembbut neitherfeofnorferroris set, both functions fall through toSUCCEED_RETURN— a short transfer reported as complete success. aksl_fwrite's error message reads"Error reading file"(src/stdlib.c:83), and checkingfeof()on a write path is meaningless.
-
aksl_sprintfwrapsvsprintf, which is unbounded. There is no way for a caller to bound the destination. Addaksl_snprintf/aksl_vsnprintfand consider deprecating thesprintfwrapper, or reject it outright — an "error-handling" wrapper around an unbounded write is a sharp edge the library exists to remove. -
No
formatattribute on the variadic wrappers. Adding__attribute__((format(printf, 2, 3)))(and the equivalents) restores the compile-time format/argument checking that callers lose by going through the wrapper. -
aksl_strhash_djb2sign-extends bytes ≥ 0x80.src/stdlib.c:181iterates achar *, which is signed on x86/ARM Linux, so high bytes contribute a sign-extended negative value and the hash differs from the canonical djb2 — and differs across platforms wherecharis unsigned. Iterate aconst unsigned char *. Also takeconst char *in the signature so callers need not cast away constness. -
aksl_tree_iteraterecursion is unbounded and cycle-blind. A deep or degenerate tree overflows the stack, and a child pointer that loops back to an ancestor recurses forever. Add a depth limit (raisingAKERR_OUTOFBOUNDS) and/or a visited check raisingAKERR_CIRCULAR_REFERENCE, consistent with what the list functions already do. -
lalloc,lfreeandqueueare dead parameters.lalloc/lfreeare defaulted and then never called;queueis entirely unused (it is the only-Wunused-parameterwarning in the file). The doc comment even tells the caller to "pass NULL here", which is a sign the queue belongs in an internal helper rather than the public signature. Either implement BFS so they are used, or drop them from the public API until it is. -
Unknown
searchmodevalues return success. Theswitchinaksl_tree_iteratehas nodefault:;searchmode == 99and the header-definedAKSL_TREE_SEARCH_VISIT(5) both fall straight through toSUCCEED_RETURNhaving visited nothing. Adddefault: FAIL_RETURN(e, AKERR_VALUE, ...). -
AKSL_TREE_SEARCH_BFS/BFS_RIGHTare unimplemented (AKERR_NOT_IMPLEMENTED), andAKSL_TREE_SEARCH_VISITis declared in the header with a documented meaning but no implementation anywhere. -
aksl_memset's andaksl_memcpy's inner checks are dead code.memsetreturnssandmemcpyreturnsd, both unconditionally; neither can fail. TheFAIL_ZERO_RETURN(e, memset(...), errno, ...)and(memcpy(...) == d)checks can never fire and only obscure the intent. Also,memcpywith overlapping ranges is UB — either document that or dispatch tomemmove. -
aksl_list_popcannot tell the caller the new head. Popping the head leaves the caller's head pointer dangling at a now-detached node. Add an out-param for the new head, or aaksl_list_pop(aksl_ListNode **head, aksl_ListNode *node)form. -
aksl_freedoes not clear the caller's pointer, so double-free remains easy. Consider anaksl_freep(void **ptr)that frees and NULLs. -
No initialisers for the public structs. Every caller must remember to
memsetanaksl_ListNode/aksl_TreeNodeto zero before use (both existing tests do). Addaksl_list_node_init/aksl_tree_node_initorAKSL_LIST_NODE_INITmacros. -
aksl_TreeNode.parentis declared but never set or read by any library function. -
Header hygiene: no
extern "C" { }guard for C++ consumers;akstdlib.hpulls instdio.h/stdlib.h/string.h/stdint.hinto every consumer's namespace. The version macros this item also asked for are done:AKSL_VERSION_MAJOR/_MINOR/_PATCH/_STRING/_NUMBER/_SONAMEare generated intoakstdlib_version.hfromproject(akstdlib VERSION …), withaksl_version()/aksl_version_string()/aksl_version_soname()andAKSL_VERSION_CHECK()reporting the loaded library so a header/.somismatch is nameable. Seetests/test_version.candREADME.md. -
Doxygen coverage is 2 functions out of 24. A
Doxyfileexists but onlyaksl_tree_iterateandaksl_list_iteratehave doc comments. Every public function needs@param/@throws/@return, especially the ones whose contract deviates from libc (aksl_free(NULL)is an error;aksl_atoiwill become strict). The fouraksl_version_*entry points have prose comments in the header but no Doxygen tags.
2.3 Build, CI and repository
CMakeLists.txt:4-6setsCMAKE_CXX_FLAGSin a C-only project, so-g -ggdb -pgnever reach the C compiler. UseCMAKE_C_FLAGS— or better, drop-pgfrom the default build entirely (it is what produces the straygmon.outsitting untracked in the repo root) and letCMAKE_BUILD_TYPEcontrol debug info.- No warning flags. Add
-Wall -Wextra(and ideally-Werrorin CI). The only current warning is the unusedqueueparameter, so the cost of turning them on is low. src/stdlib.ctriggers "ISO C99 requires at least one argument for the..." on roughly 20 lines under-Wpedantic, becauseFAIL_*is called with a bare message and no varargs. Either always pass an argument, or add a zero-arg-safe form in libakerror.- The
deps/libakerrorsubmodule was pinned 22 commits behindmain(pinned at4fad0ce "Add gitea workflow"). Bumped to5ff8790, which is libakerror 1.0.0 — the release that made the status-name table private, moved consumer status codes to a band starting atAKERR_FIRST_CONSUMER_STATUS(256), made range ownership enforced, and gave the library an soname. Seedeps/libakerror/UPGRADING.md. The bump also brings the refcount-leak fix, the stack-trace buffer-overflow fix and the format-string fix. Nothing in this library's sources used the removedAKERR_MAX_ERR_VALUE,__AKERR_ERROR_NAMES,AKERR_STATUS_RANGE_OKorAKERR_STATUS_NAME_OK, so the source change was confined to the build, the packaging and one compile-time guard. What it did move is recorded in the three items below. - libakerror does not namespace its
coveragetarget when embedded, only itsmutationtarget (deps/libakerror/CMakeLists.txt:172vs:189). A-DAKSL_COVERAGE=ONtop-level build therefore failed to configure at all — "another target with the same name already exists" — the moment the dependency gained a coverage target. Worked around inCMakeLists.txtby shadowingadd_custom_targetfor the duration of theadd_subdirectory()call and renaming the dependency's toakerror_coverage, alongside the existingadd_testshadow. Fix upstream and delete the workaround: libakerror should apply the sameCMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIRtest tocoveragethat it already applies tomutation. - Branch coverage of
src/stdlib.cfell from 51.0% to 44.3% on the 1.0.0 bump, with no change to this library's sources or tests. Line coverage held at 99.0% (200/202) and function coverage at 100% (21/21); the branch denominator went from 661 to 1087 because the 1.0.0PREPARE_ERROR/FAIL_*macros expand to more branches at every call site. 337/661 became 481/1087 — 144 more branches covered, 426 more counted. The CI branch gate was re-ratcheted 45 → 40 to match. Most of the added branches are unreachable from the way this library calls the macros, so raising the number here would mean testing libakerror's macros rather than this library; that belongs to libakerror's mutation suite, since macros expand at the call site and coverage cannot see them properly from either side. Revisit if the gap ever hides a real regression. project()declared noVERSION, so@PROJECT_VERSION@expanded to nothing, the installedakstdlib.pcshipped an emptyVersion:field, andlibakstdlib.socarried no soname. Nowproject(akstdlib VERSION 0.1.0), withSOVERSION0.1(MAJOR.MINORwhile major is 0,MAJORfrom 1.0 — theif()inCMakeLists.txtand the matching#ifintests/test_version.cencode the rule and are tested against each other), anakstdlibConfigVersion.cmakeatSameMinorVersion, and version macros generated intoakstdlib_version.h. Verified:find_package(akstdlib 0.1)is accepted while0.2and1.0are refused, and a 0.2.0 build dropped in under the 0.1 filename is caught byAKSL_VERSION_CHECK().- libakerror still installs no
akerrorConfigVersion.cmake, socmake/akstdlib.cmake.inhas to callfind_dependency(akerror)with no version — a request for one would be refused for want of a version file regardless of what is installed. libakstdlib now does this correctly and libakerror does not; fix it there with the samewrite_basic_package_version_file()call, then add the1.0.0floor to thefind_dependencyhere. Until then the floor rests onakstdlib.pc'sRequires:and the#errorguard inakstdlib.h. aksl_version_check()ignores itspatchargument (src/stdlib.c, the(void)patch), which is correct for the current "same soname" rule but means the parameter exists only so the error message can name the caller's full version. If a future rule needs patch to participate, that is the line to change.- CI does not build against the submodule it pins.
.gitea/workflows/ci.yamlcloneslibakerror@mainand installs it, while the build it then runs is top-level and so compilesdeps/libakerrorat the pinned commit — two different libakerror versions depending on where you build, and the installed one is never actually linked. Pick one. (The submodule is at least present in CI now: §1.0 addedsubmodules: recursiveto the checkout, without which configure failed outright.) ctestwas red (treefailing plus four Not Run submodule tests) — fixed in §1.0. The build badge inREADME.mdmeans something again.- Untracked build litter in the repo root:
build/,gmon.out,CMakeLists.txt-akstdlib, and a dozen*~editor backups. Add a.gitignore(libakerror has one; this repo does not). README.mdis a build badge and nothing else. It needs at minimum: what the library is, the akerror wrapper contract, the list of wrapped functions, and the deviations from libc semantics.- Indentation is inconsistent —
src/stdlib.cmixes hard tabs and 4-space indents within the same functions. libakerror standardised on Stroustrup style (commite5f7616); apply the same here, ideally with a checked-in.clang-format.
3. libc functions not yet wrapped
Ordered roughly by how much a caller of this library would miss them. Each entry means "add
an aksl_-prefixed wrapper that turns the documented failure modes into an
akerr_ErrorContext".
3.1 High priority — gaps in areas the library already covers
Memory (stdlib.h, string.h)
calloc,realloc,reallocarray—realloc's "returns NULL and the old pointer is still valid" trap is exactly what this library should be hiding.aligned_alloc,posix_memalignmemmove,memcmp,memchr
Bounded string formatting (stdio.h)
snprintf,vsnprintf— needed to make §2.2.4 fixable.vprintf,vfprintf,vsprintf— theva_listforms, so consumers can build their own variadic wrappers on top.asprintf/vasprintf(GNU) as an allocating alternative.
String → number (stdlib.h)
strtol,strtoll,strtoul,strtoull,strtod,strtof,strtold— the correct foundation for §2.1.5.
Strings (string.h)
strlen,strnlenstrcpy,strncpy,strcat,strncat— with truncation reported as an error rather than silently accepted, which is the whole value proposition here.strdup,strndupstrcmp,strncmp,strcasecmp,strncasecmp,strcollstrchr,strrchr,strstr,strcasestr,strpbrk,strspn,strcspnstrtok_r,strsepstrerror_r
Stream I/O (stdio.h)
fseek,ftell,rewind,fgetpos,fsetpos,fseeko,ftellofflushfgets,fputs,fgetc/getc/getchar,fputc/putc/putchar,ungetcgetline,getdelimfreopen,fdopen,filenosetvbuf,setbufclearerr,feof,ferror— thin, but worth exposing so callers never touch rawFILE *internals.sscanf,fscanf,scanf— the return-value semantics (items matched vsEOF) are a classic silent-failure source.remove,rename,tmpfile,mkstemp,mkdtempperror— or an akerror-native equivalent.
3.2 POSIX file and process API (likely the next major surface)
unistd.h / fcntl.h
open,close,read,write,pread,pwrite,lseekreadv,writevdup,dup2,pipe,fcntlfsync,fdatasync,truncate,ftruncateunlink,link,symlink,readlink,rmdir,mkdiraccess,faccessat,chmod,fchmod,chown,fchown,umaskchdir,fchdir,getcwdisatty,ttyname_rsysconf,pathconfsleep,usleep,nanosleep
sys/stat.h
stat,fstat,lstat,fstatatstatvfs,fstatvfs
dirent.h
opendir,fdopendir,readdir,readdir_r,closedir,rewinddir,scandir
Process control
fork,execve/execvp/execlfamily,waitpid,waitposix_spawnsystem,popen,pclosegetpid,getppid,getuid,geteuid,setuid,setgidatexit,exit,_exit,abort— mostly to give akerror a hook on shutdown.getenv,setenv,unsetenv,putenv,clearenv
sys/mman.h
mmap,munmap,mprotect,msync,madvise
3.3 Time
time,clock_gettime,clock_getres,gettimeofdaylocaltime_r,gmtime_r,mktime,timegm,difftimestrftime,strptimeclock,times
3.4 Sorting, searching and misc stdlib.h
qsort,qsort_r,bsearch— comparator errors currently have nowhere to go; an akerror-aware comparator signature would be a genuine improvement.abs,labs,llabs,div,ldiv,lldivrand,srand,random,srandom,getrandom/arc4random
3.5 Lower priority / larger projects
- Sockets:
socket,bind,listen,accept,connect,send/sendto/sendmsg,recv/recvfrom/recvmsg,shutdown,setsockopt/getsockopt,getaddrinfo/freeaddrinfo/gai_strerror,inet_ntop/inet_pton - Multiplexing:
select,poll,ppoll,epoll_create1/epoll_ctl/epoll_wait - Signals:
sigaction,sigprocmask,sigemptyset/sigaddset,kill,raise,signalfd - Threads:
pthread_create/join/detach,pthread_mutex_*,pthread_cond_*,pthread_rwlock_*,sem_*— blocked on resolving the thread-safety question in §1.9 (libakerror's error pool is an unlocked process-global array). - Dynamic loading:
dlopen,dlsym,dlclose,dlerror - Locale / wide chars:
setlocale,mbstowcs,wcstombs,iconv_* - Math: the
math.hfunctions that seterrno/raise FP exceptions (sqrt,log,pow,acos, …) — probably better served by a dedicatedlibakmath.
3.6 Non-libc additions the current data structures imply
Not libc wrappers, but the existing list/tree API is visibly incomplete:
- List:
prepend,insert_after/insert_before,length,find,reverse,concat,free_all, reverse iteration, and a head/tail-tracking container type soappendis O(1) instead of O(n). - Tree:
insert,remove,find,height,count,free_all, and the BFS traversal thelalloc/lfree/queueparameters were designed for. - Hash map built on
aksl_strhash_djb2, plus additional hashes (FNV-1a, xxHash) and a NUL-terminatedaksl_strhash_djb2_strconvenience form. - Growable buffer / string-builder type, to make the
snprintfandstrcatwrappers 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
-
A strict
strtoll/strtodwrapper (akbasic/src/convert.c, ~60 lines). This is §2.1.5 biting, and it is not cosmetic: the Go reference this port reproduces checksstrconv's error at four sites and turns each into a BASIC error. Routing them throughaksl_atoiwould have converted four diagnosable errors into wrong answers —VAL("garbage")silently returning0.0rather than raising, and a malformed line number becoming line 0. akbasic's ownTODO.md§1.9 formally bans theaksl_ato*family for this reason. The fix wanted is exactly §3.1'sstrtolfamily plus the endptr/errno/range contract §2.1.5 describes; when it lands,src/convert.cgets deleted. -
A fixed-capacity string-keyed hash table (
akbasic/src/symtab.c, ~130 lines). §3.6 already calls for a "hash map built onaksl_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. -
The bounded-copy-with-truncation-as-error idiom, ten times.
strncpyfollowed by an explicit NUL and a preceding length check appears at ten sites across six files. §3.1 listsstrncpy"with truncation reported as an error rather than silently accepted, which is the whole value proposition here" — agreed, and the repetition is the evidence. -
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:197andsrc/parser_commands.c:113, and through<ctype.h>'stoupperinsrc/verbs.c. §3.1'sstrcasecmp/strncasecmpwould remove all three.
4.2 Confirmed with impact
- §2.2.4 (
aksl_sprintfis unbounded) — akbasic never calls it, deliberately. Its 28snprintfcalls all write into fixed buffers whose size is the whole point, and there is noaksl_snprintfto route them through. This is the single highest-value item in §3.1 for a consumer of this shape. - §2.2.2 (
aksl_fopendoes not validatepathname/mode) — real, and reached from user input. akbasic'sDLOAD/DSAVEtake a filename from a BASIC string expression, so an empty or unevaluated one is reachable from a running program;akbasic_cmd_dloadvalidates the name itself before handing it over, with a comment pointing here. - §2.2.6 (
aksl_strhash_djb2sign-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_endis never called) — akbasic's stdio text sink is built onaksl_fprintfand 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.