Files
libakstdlib/src/collections.c

1131 lines
39 KiB
C
Raw Normal View History

Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
/*
* Data structures.
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
*
* Not libc wrappers. These are the parts of the list and tree API that were
* visibly missing, plus the two structures the first real consumer had to write
* for itself: a string-keyed hash table (akbasic's src/symtab.c, ~130 lines,
* needed three times over for variables, functions and labels) and a growable
* string buffer, without which the bounded formatting wrappers are unpleasant to
* use for anything of unknown length.
*
* Two things hold throughout.
*
* Nothing here allocates unless its name says so. The list and tree functions
* relink nodes the caller already owns; aksl_list_free_all and
* aksl_tree_free_all take the free function to use, so a caller drawing from a
* fixed pool can hand over its own. Only aksl_strbuf_* owns memory, and it says
* so in the type name.
*
* The hash map is fixed-capacity and refuses rather than resizes when full,
* which is the same decision akbasic made and for the same reason: a table that
* silently reallocates is a table whose pointers move underneath you, and one
* that cannot grow is one whose worst case you can state.
*/
#include <akstdlib.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "aksl_internal.h"
/* ====================================================================== */
/* Linked list */
/* ====================================================================== */
/*
* Walk to the tail, refusing a cyclic list. Shared by the functions below that
* need the end; the bound is Floyd's, exactly as aksl_list_append does it, so
* every whole-list walk in this library fails the same way on the same input.
*/
static akerr_ErrorContext AKERR_NOIGNORE *list_tail(aksl_ListNode *head, aksl_ListNode **dest)
{
aksl_ListNode *slow = head;
aksl_ListNode *fast = head;
aksl_ListNode *tail = head;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
*dest = NULL;
while ( fast != NULL && fast->next != NULL ) {
slow = slow->next;
fast = fast->next->next;
if ( fast == slow ) {
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", (void *)head);
}
}
while ( tail->next != NULL ) {
tail = tail->next;
}
*dest = tail;
SUCCEED_RETURN(e);
}
/*
* obj becomes the new head. Takes the head by reference for the same reason
* aksl_list_pop does: the caller's own pointer has to move, and there is no way
* to do that from a node pointer alone.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_prepend(aksl_ListNode **head, aksl_ListNode *obj)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head=%p, obj=%p", (void *)head, (void *)obj);
FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "head=%p, obj=%p", (void *)head, (void *)obj);
FAIL_NONZERO_RETURN(e, (*head == obj), AKERR_VALUE,
"obj %p is already the head of this list", (void *)obj);
obj->prev = NULL;
obj->next = *head;
if ( *head != NULL ) {
(*head)->prev = obj;
}
*head = obj;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_after(aksl_ListNode *node, aksl_ListNode *obj)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node=%p, obj=%p", (void *)node, (void *)obj);
FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "node=%p, obj=%p", (void *)node, (void *)obj);
FAIL_NONZERO_RETURN(e, (node == obj), AKERR_VALUE,
"cannot insert node %p after itself", (void *)obj);
obj->prev = node;
obj->next = node->next;
if ( node->next != NULL ) {
node->next->prev = obj;
}
node->next = obj;
SUCCEED_RETURN(e);
}
/*
* head is required because inserting before the current head moves it, which is
* the case a caller is most likely to get wrong by hand.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_before(aksl_ListNode **head,
aksl_ListNode *node,
aksl_ListNode *obj)
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head=%p, node=%p, obj=%p",
(void *)head, (void *)node, (void *)obj);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "head=%p, node=%p, obj=%p",
(void *)head, (void *)node, (void *)obj);
FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "head=%p, node=%p, obj=%p",
(void *)head, (void *)node, (void *)obj);
FAIL_NONZERO_RETURN(e, (node == obj), AKERR_VALUE,
"cannot insert node %p before itself", (void *)obj);
obj->next = node;
obj->prev = node->prev;
if ( node->prev != NULL ) {
node->prev->next = obj;
}
node->prev = obj;
if ( *head == node ) {
*head = obj;
}
SUCCEED_RETURN(e);
}
/* A NULL head is an empty list of length 0, not an error. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_length(aksl_ListNode *head, size_t *dest)
{
aksl_ListNode *slow = head;
aksl_ListNode *fast = head;
aksl_ListNode *walk = head;
size_t n = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
*dest = 0;
while ( fast != NULL && fast->next != NULL ) {
slow = slow->next;
fast = fast->next->next;
if ( fast == slow ) {
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", (void *)head);
}
}
while ( walk != NULL ) {
n++;
walk = walk->next;
}
*dest = n;
SUCCEED_RETURN(e);
}
/*
* The first node the predicate accepts, or NULL if none does -- absent is an
* answer, as everywhere else in this library. The predicate reports through an
* out-param and may raise, in which case the search stops and the error
* propagates with its message intact.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_find(aksl_ListNode *head,
aksl_ListNodePredicate pred,
void *data,
aksl_ListNode **dest)
{
aksl_ListNode *walk = head;
aksl_ListNode *tail = NULL;
int matched = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, pred, AKERR_NULLPOINTER, "pred=%p, dest=%p", (void *)pred, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pred=%p, dest=%p", (void *)pred, (void *)dest);
*dest = NULL;
/* Bound the walk before making it, so a cyclic list cannot spin forever. */
if ( head != NULL ) {
PASS(e, list_tail(head, &tail));
}
while ( walk != NULL ) {
PASS(e, pred(walk, data, &matched));
if ( matched != 0 ) {
*dest = walk;
break;
}
walk = walk->next;
}
SUCCEED_RETURN(e);
}
/* Reverses in place, swapping every node's links and moving the caller's head. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_reverse(aksl_ListNode **head)
{
aksl_ListNode *walk = NULL;
aksl_ListNode *prev = NULL;
aksl_ListNode *next = NULL;
aksl_ListNode *tail = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head");
if ( *head == NULL ) {
SUCCEED_RETURN(e);
}
PASS(e, list_tail(*head, &tail));
walk = *head;
while ( walk != NULL ) {
next = walk->next;
walk->next = prev;
walk->prev = next;
prev = walk;
walk = next;
}
*head = prev;
SUCCEED_RETURN(e);
}
/*
* Links `other` onto the end of `head`. Both must be acyclic and disjoint --
* concatenating a list with itself, or with something already inside it, would
* make a cycle, so the tail walk that finds the join point also refuses it.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_concat(aksl_ListNode *head, aksl_ListNode *other)
{
aksl_ListNode *tail = NULL;
aksl_ListNode *walk = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head=%p, other=%p", (void *)head, (void *)other);
if ( other == NULL ) {
SUCCEED_RETURN(e);
}
FAIL_NONZERO_RETURN(e, (head == other), AKERR_VALUE,
"cannot concatenate a list with itself");
PASS(e, list_tail(head, &tail));
for ( walk = head; walk != NULL; walk = walk->next ) {
FAIL_NONZERO_RETURN(e, (walk == other), AKERR_VALUE,
"node %p is already in the destination list", (void *)other);
}
tail->next = other;
other->prev = tail;
SUCCEED_RETURN(e);
}
/*
* Releases every node through lfree, or through aksl_free when it is NULL, and
* clears the caller's head. The next pointer is read before the node goes, which
* is the whole trick; freeing forwards without that reads freed memory on the
* very next iteration.
*
* A failure part-way through does not abandon the rest of the list: the first
* error is kept and returned once the walk is finished, so a bad free cannot
* turn into a leak of everything after it.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_free_all(aksl_ListNode **head, aksl_FreeFunc lfree)
{
aksl_ListNode *walk = NULL;
aksl_ListNode *next = NULL;
aksl_ListNode *tail = NULL;
akerr_ErrorContext *first = NULL;
akerr_ErrorContext *raised = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head");
if ( lfree == NULL ) {
lfree = &aksl_free;
}
if ( *head == NULL ) {
SUCCEED_RETURN(e);
}
PASS(e, list_tail(*head, &tail));
walk = *head;
*head = NULL;
while ( walk != NULL ) {
next = walk->next;
raised = lfree(walk);
if ( raised != NULL && first == NULL ) {
first = raised;
} else if ( raised != NULL ) {
raised = akerr_release_error(raised);
}
walk = next;
}
if ( first != NULL ) {
return first;
}
SUCCEED_RETURN(e);
}
/*
* Iterates from `tail` back towards the head. Given a tail rather than a head
* because that is what the caller has after a walk forwards, and because a
* doubly-linked list that cannot be read backwards is just a linked list.
* AKERR_ITERATOR_BREAK stops it, as it does going the other way.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate_reverse(aksl_ListNode *tail,
aksl_ListNodeIterator iter,
void *data)
{
aksl_ListNode *slow = tail;
aksl_ListNode *fast = tail;
aksl_ListNode *node = tail;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, tail, AKERR_NULLPOINTER, "tail=%p, iter=%p", (void *)tail, (void *)iter);
FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "tail=%p, iter=%p", (void *)tail, (void *)iter);
while ( fast != NULL && fast->prev != NULL ) {
slow = slow->prev;
fast = fast->prev->prev;
if ( fast == slow ) {
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", (void *)tail);
}
}
while ( node != NULL ) {
ATTEMPT {
CATCH(e, iter(node, data));
node = node->prev;
} CLEANUP {
} PROCESS(e) {
} HANDLE(e, AKERR_ITERATOR_BREAK) {
// Not an error condition; it is telling us to stop early
SUCCEED_RETURN(e);
} FINISH(e, true);
}
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* The tracked list container */
/* ---------------------------------------------------------------------- */
/*
* aksl_list_append has to walk the whole list to find the tail, so building a
* list of n nodes with it is O(n^2). That is fine for the handful of nodes the
* bare-node API was written for and wrong for anything larger, which is what
* the collections plan meant by "a head/tail-tracking container type so append is O(1)".
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
*
* The container holds the length as well, so aksl_list_length stops being a
* walk. It owns no memory -- the nodes are still the caller's -- so there is no
* aksl_list_destroy; use aksl_list_clear if the nodes need releasing.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_init(aksl_List *list)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list");
list->head = NULL;
list->tail = NULL;
list->length = 0;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_push(aksl_List *list, aksl_ListNode *obj)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list=%p, obj=%p", (void *)list, (void *)obj);
FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "list=%p, obj=%p", (void *)list, (void *)obj);
obj->next = NULL;
obj->prev = list->tail;
if ( list->tail != NULL ) {
list->tail->next = obj;
} else {
list->head = obj;
}
list->tail = obj;
list->length += 1;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_unshift(aksl_List *list, aksl_ListNode *obj)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list=%p, obj=%p", (void *)list, (void *)obj);
FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "list=%p, obj=%p", (void *)list, (void *)obj);
obj->prev = NULL;
obj->next = list->head;
if ( list->head != NULL ) {
list->head->prev = obj;
} else {
list->tail = obj;
}
list->head = obj;
list->length += 1;
SUCCEED_RETURN(e);
}
/*
* Unlinks a node the caller already holds. It must be in this list -- the
* container's length and endpoints would otherwise silently stop describing it
* -- so removing a node that is not is AKERR_VALUE rather than quiet corruption.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_remove(aksl_List *list, aksl_ListNode *node)
{
aksl_ListNode *walk = NULL;
int found = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list=%p, node=%p", (void *)list, (void *)node);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "list=%p, node=%p", (void *)list, (void *)node);
for ( walk = list->head; walk != NULL; walk = walk->next ) {
if ( walk == node ) {
found = 1;
break;
}
}
FAIL_ZERO_RETURN(e, found, AKERR_VALUE, "node %p is not in this list", (void *)node);
if ( node->prev != NULL ) {
node->prev->next = node->next;
} else {
list->head = node->next;
}
if ( node->next != NULL ) {
node->next->prev = node->prev;
} else {
list->tail = node->prev;
}
node->next = NULL;
node->prev = NULL;
list->length -= 1;
SUCCEED_RETURN(e);
}
/* Empties the container, releasing every node through lfree (aksl_free if NULL). */
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_clear(aksl_List *list, aksl_FreeFunc lfree)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list");
ATTEMPT {
CATCH(e, aksl_list_free_all(&list->head, lfree));
} CLEANUP {
/* The container is emptied whether or not every node released cleanly;
* leaving it describing nodes that are half gone would be worse. */
list->head = NULL;
list->tail = NULL;
list->length = 0;
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/* ====================================================================== */
/* Binary search tree */
/* ====================================================================== */
/*
* An ordered tree needs an ordering, so these take a comparator. It reports
* through an out-param and may raise, like every other callback here.
*
* These are the functions that set and read aksl_TreeNode.parent, which was
* declared and then never touched by anything in the library.
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
* aksl_tree_remove needs it: relinking a node's replacement means telling that
* node's parent about it, and finding the parent by walking from the root again
* would turn a removal into a second search.
*
* This is a plain unbalanced BST. Inserting already-sorted data gives a
* degenerate chain, which aksl_tree_iterate then refuses past
* AKSL_TREE_MAX_DEPTH -- so it is bounded rather than dangerous, but it is not
* a balanced tree and does not pretend to be.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_insert(aksl_TreeNode **root,
aksl_TreeNode *node,
aksl_TreeCompareFunc cmp)
{
aksl_TreeNode *walk = NULL;
int order = 0;
int depth = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root=%p, node=%p, cmp=%p",
(void *)root, (void *)node, (void *)cmp);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "root=%p, node=%p, cmp=%p",
(void *)root, (void *)node, (void *)cmp);
FAIL_ZERO_RETURN(e, cmp, AKERR_NULLPOINTER, "root=%p, node=%p, cmp=%p",
(void *)root, (void *)node, (void *)cmp);
node->left = NULL;
node->right = NULL;
node->parent = NULL;
if ( *root == NULL ) {
*root = node;
SUCCEED_RETURN(e);
}
walk = *root;
while ( 1 ) {
FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS,
"tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH);
depth++;
PASS(e, cmp(node->leaf, walk->leaf, &order));
if ( order < 0 ) {
if ( walk->left == NULL ) {
walk->left = node;
node->parent = walk;
break;
}
walk = walk->left;
} else {
/*
* Equal keys go right, so insertion order is preserved among them
* and a duplicate is stored rather than refused. A caller who wants
* uniqueness checks with aksl_tree_find first.
*/
if ( walk->right == NULL ) {
walk->right = node;
node->parent = walk;
break;
}
walk = walk->right;
}
}
SUCCEED_RETURN(e);
}
/* NULL and success when the key is not present. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_find(aksl_TreeNode *root, void *leaf,
aksl_TreeCompareFunc cmp,
aksl_TreeNode **dest)
{
aksl_TreeNode *walk = root;
int order = 0;
int depth = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, cmp, AKERR_NULLPOINTER, "cmp=%p, dest=%p", (void *)cmp, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "cmp=%p, dest=%p", (void *)cmp, (void *)dest);
*dest = NULL;
while ( walk != NULL ) {
FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS,
"tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH);
depth++;
PASS(e, cmp(leaf, walk->leaf, &order));
if ( order == 0 ) {
*dest = walk;
break;
}
walk = (order < 0) ? walk->left : walk->right;
}
SUCCEED_RETURN(e);
}
/* Attach `child` (which may be NULL) where `node` used to hang off its parent. */
static void tree_replace(aksl_TreeNode **root, aksl_TreeNode *node, aksl_TreeNode *child)
{
if ( node->parent == NULL ) {
*root = child;
} else if ( node->parent->left == node ) {
node->parent->left = child;
} else {
node->parent->right = child;
}
if ( child != NULL ) {
child->parent = node->parent;
}
}
/*
* The textbook three cases: no children, one child, two children. The last is
* the interesting one -- the node's in-order successor (leftmost of the right
* subtree) takes its place, because that is the only value that keeps every
* ordering invariant on both sides.
*
* The removed node's own links are cleared, so it can go straight back into a
* pool or be inserted somewhere else without carrying stale pointers.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_remove(aksl_TreeNode **root, aksl_TreeNode *node)
{
aksl_TreeNode *successor = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root=%p, node=%p", (void *)root, (void *)node);
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "root=%p, node=%p", (void *)root, (void *)node);
FAIL_ZERO_RETURN(e, *root, AKERR_VALUE, "the tree is empty");
if ( node->left == NULL ) {
tree_replace(root, node, node->right);
} else if ( node->right == NULL ) {
tree_replace(root, node, node->left);
} else {
successor = node->right;
while ( successor->left != NULL ) {
successor = successor->left;
}
if ( successor->parent != node ) {
tree_replace(root, successor, successor->right);
successor->right = node->right;
successor->right->parent = successor;
}
tree_replace(root, node, successor);
successor->left = node->left;
successor->left->parent = successor;
}
node->parent = NULL;
node->left = NULL;
node->right = NULL;
SUCCEED_RETURN(e);
}
/* Height in nodes: an empty tree is 0, a single node is 1. */
static akerr_ErrorContext AKERR_NOIGNORE *tree_height(aksl_TreeNode *root, int depth, int *dest)
{
int left = 0;
int right = 0;
PREPARE_ERROR(e);
FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS,
"tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH);
if ( root == NULL ) {
*dest = 0;
SUCCEED_RETURN(e);
}
PASS(e, tree_height(root->left, depth + 1, &left));
PASS(e, tree_height(root->right, depth + 1, &right));
*dest = 1 + (left > right ? left : right);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_height(aksl_TreeNode *root, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
*dest = 0;
return tree_height(root, 0, dest);
}
static akerr_ErrorContext AKERR_NOIGNORE *tree_count(aksl_TreeNode *root, int depth, size_t *dest)
{
size_t left = 0;
size_t right = 0;
PREPARE_ERROR(e);
FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS,
"tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH);
if ( root == NULL ) {
*dest = 0;
SUCCEED_RETURN(e);
}
PASS(e, tree_count(root->left, depth + 1, &left));
PASS(e, tree_count(root->right, depth + 1, &right));
*dest = 1 + left + right;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_count(aksl_TreeNode *root, size_t *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
*dest = 0;
return tree_count(root, 0, dest);
}
/*
* Post-order, necessarily: a node's children have to be released before the
* node that points at them, and any other order reads freed memory to find the
* second subtree. The first failure is kept and returned at the end rather than
* abandoning the rest of the tree, as aksl_list_free_all does.
*/
static akerr_ErrorContext AKERR_NOIGNORE *tree_free_all(aksl_TreeNode *root, int depth,
aksl_FreeFunc lfree,
akerr_ErrorContext **first)
{
aksl_TreeNode *left = NULL;
aksl_TreeNode *right = NULL;
akerr_ErrorContext *raised = NULL;
PREPARE_ERROR(e);
FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS,
"tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH);
if ( root == NULL ) {
SUCCEED_RETURN(e);
}
left = root->left;
right = root->right;
PASS(e, tree_free_all(left, depth + 1, lfree, first));
PASS(e, tree_free_all(right, depth + 1, lfree, first));
raised = lfree(root);
if ( raised != NULL && *first == NULL ) {
*first = raised;
} else if ( raised != NULL ) {
raised = akerr_release_error(raised);
}
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_free_all(aksl_TreeNode **root, aksl_FreeFunc lfree)
{
akerr_ErrorContext *first = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root");
if ( lfree == NULL ) {
lfree = &aksl_free;
}
ATTEMPT {
CATCH(e, tree_free_all(*root, 0, lfree, &first));
} CLEANUP {
*root = NULL;
} PROCESS(e) {
} FINISH(e, true);
if ( first != NULL ) {
return first;
}
SUCCEED_RETURN(e);
}
/* ====================================================================== */
/* Hashes */
/* ====================================================================== */
/*
* FNV-1a, the other half of the collections plan's hash request. It differs from djb2 in
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
* XOR-then-multiply rather than multiply-then-add, which mixes the low bits
* rather better -- worth having when the keys are short and share a prefix,
* which is exactly what identifiers in a symbol table look like.
*
* Bytes are read unsigned here for the same reason they are in djb2: a
* sign-extended byte would make the hash depend on whether plain char happens to
* be signed on the target.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a(const char *str, size_t len, uint32_t *hashval)
{
const unsigned char *cursor = NULL;
uint32_t h = 2166136261u; /* FNV offset basis, 32-bit */
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval);
FAIL_ZERO_RETURN(e, hashval, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval);
cursor = (const unsigned char *)str;
while ( len-- ) {
h ^= (uint32_t)*cursor++;
h *= 16777619u; /* FNV prime, 32-bit */
}
*hashval = h;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint32_t *hashval)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval);
return aksl_strhash_fnv1a(str, strlen(str), hashval);
}
/* ====================================================================== */
/* Hash map */
/* ====================================================================== */
/*
* Fixed-capacity, open-addressed, linear-probing, string-keyed.
*
* The shape is akbasic's src/symtab.c, which the collections plan called "worth lifting
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
* more or less verbatim": the caller supplies the slot array, the map refuses
* rather than resizes when full, and the keys are copied into fixed-size slots
* so the map owns them and a caller cannot outlive its own key strings.
*
* Refusing rather than resizing is the deliberate part. A table that reallocates
* is a table whose entry pointers move underneath anything holding one; a table
* that cannot grow has a worst case you can state and a failure you can see. The
* cost is that the caller has to size it, which is why aksl_hashmap_init takes
* the slot array rather than allocating one.
*
* Deletion leaves a tombstone rather than an empty slot, because clearing the
* slot outright would break the probe chain of anything that hashed past it.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_init(aksl_HashMap *map,
aksl_HashEntry *slots,
size_t capacity)
{
size_t i = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, slots=%p", (void *)map, (void *)slots);
FAIL_ZERO_RETURN(e, slots, AKERR_NULLPOINTER, "map=%p, slots=%p", (void *)map, (void *)slots);
FAIL_ZERO_RETURN(e, capacity, AKERR_VALUE, "capacity=0");
for ( i = 0; i < capacity; i++ ) {
slots[i].state = AKSL_HASHMAP_SLOT_EMPTY;
slots[i].key[0] = '\0';
slots[i].value = NULL;
}
map->slots = slots;
map->capacity = capacity;
map->count = 0;
SUCCEED_RETURN(e);
}
/*
* Find the slot a key belongs in.
*
* *found is the index of the key if it is present. *insert is where it would go
* if it is not -- the first tombstone seen, or the empty slot the probe stopped
* at -- so a put that has to insert does not have to probe twice. Either may
* come back as capacity, meaning "no such thing".
*/
static akerr_ErrorContext AKERR_NOIGNORE *hashmap_probe(aksl_HashMap *map, const char *key,
size_t *found, size_t *insert)
{
uint32_t h = 0;
size_t idx = 0;
size_t probed = 0;
PREPARE_ERROR(e);
*found = map->capacity;
*insert = map->capacity;
PASS(e, aksl_strhash_djb2_str(key, &h));
idx = (size_t)h % map->capacity;
for ( probed = 0; probed < map->capacity; probed++ ) {
if ( map->slots[idx].state == AKSL_HASHMAP_SLOT_EMPTY ) {
if ( *insert == map->capacity ) {
*insert = idx;
}
/* An empty slot ends the probe chain: the key is not here. */
break;
}
if ( map->slots[idx].state == AKSL_HASHMAP_SLOT_DELETED ) {
if ( *insert == map->capacity ) {
*insert = idx;
}
} else if ( strcmp(map->slots[idx].key, key) == 0 ) {
*found = idx;
break;
}
idx = (idx + 1) % map->capacity;
}
SUCCEED_RETURN(e);
}
/*
* Inserts, or replaces the value of an existing key. A key longer than
* AKSL_HASHMAP_MAX_KEY-1 is AKERR_OUTOFBOUNDS rather than a truncated key that
* would collide with a different one sharing its prefix. A full map is
* AKERR_OUTOFBOUNDS too, naming the capacity it hit.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_put(aksl_HashMap *map, const char *key, void *value)
{
size_t found = 0;
size_t insert = 0;
size_t keylen = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, key=%p", (void *)map, (void *)key);
FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "map=%p, key=%p", (void *)map, (void *)key);
FAIL_ZERO_RETURN(e, map->slots, AKERR_NULLPOINTER, "map is not initialised");
keylen = strlen(key);
FAIL_NONZERO_RETURN(e, (keylen >= AKSL_HASHMAP_MAX_KEY), AKERR_OUTOFBOUNDS,
"key of %zu bytes exceeds AKSL_HASHMAP_MAX_KEY (%d)",
keylen, AKSL_HASHMAP_MAX_KEY);
PASS(e, hashmap_probe(map, key, &found, &insert));
if ( found != map->capacity ) {
map->slots[found].value = value;
SUCCEED_RETURN(e);
}
FAIL_NONZERO_RETURN(e, (insert == map->capacity), AKERR_OUTOFBOUNDS,
"map is full at %zu entries; it does not resize", map->capacity);
memcpy(map->slots[insert].key, key, keylen + 1);
map->slots[insert].value = value;
map->slots[insert].state = AKSL_HASHMAP_SLOT_OCCUPIED;
map->count += 1;
SUCCEED_RETURN(e);
}
/*
* A key that is not present is *found = 0 and success, not an error -- looking
* something up and not finding it is the ordinary case in a symbol table, and a
* caller should not have to catch an error to discover it. *value is written
* only when the key is there.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_get(aksl_HashMap *map, const char *key,
void **value, int *found)
{
size_t at = 0;
size_t insert = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, key=%p, found=%p",
(void *)map, (void *)key, (void *)found);
FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "map=%p, key=%p, found=%p",
(void *)map, (void *)key, (void *)found);
FAIL_ZERO_RETURN(e, found, AKERR_NULLPOINTER, "map=%p, key=%p, found=%p",
(void *)map, (void *)key, (void *)found);
FAIL_ZERO_RETURN(e, map->slots, AKERR_NULLPOINTER, "map is not initialised");
*found = 0;
FAIL_NONZERO_RETURN(e, (strlen(key) >= AKSL_HASHMAP_MAX_KEY), AKERR_OUTOFBOUNDS,
"key exceeds AKSL_HASHMAP_MAX_KEY (%d)", AKSL_HASHMAP_MAX_KEY);
PASS(e, hashmap_probe(map, key, &at, &insert));
if ( at != map->capacity ) {
*found = 1;
if ( value != NULL ) {
*value = map->slots[at].value;
}
}
SUCCEED_RETURN(e);
}
/* Removing a key that is not there is success with *removed = 0. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_remove(aksl_HashMap *map, const char *key, int *removed)
{
size_t at = 0;
size_t insert = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, key=%p", (void *)map, (void *)key);
FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "map=%p, key=%p", (void *)map, (void *)key);
FAIL_ZERO_RETURN(e, map->slots, AKERR_NULLPOINTER, "map is not initialised");
if ( removed != NULL ) {
*removed = 0;
}
FAIL_NONZERO_RETURN(e, (strlen(key) >= AKSL_HASHMAP_MAX_KEY), AKERR_OUTOFBOUNDS,
"key exceeds AKSL_HASHMAP_MAX_KEY (%d)", AKSL_HASHMAP_MAX_KEY);
PASS(e, hashmap_probe(map, key, &at, &insert));
if ( at != map->capacity ) {
/*
* A tombstone, not an empty slot: emptying it would cut the probe chain
* of every key that hashed to an earlier slot and walked past this one.
*/
map->slots[at].state = AKSL_HASHMAP_SLOT_DELETED;
map->slots[at].key[0] = '\0';
map->slots[at].value = NULL;
map->count -= 1;
if ( removed != NULL ) {
*removed = 1;
}
}
SUCCEED_RETURN(e);
}
/*
* Visits every live entry, in slot order -- which is to say in no order the
* caller can predict or should rely on. AKERR_ITERATOR_BREAK stops it, as
* everywhere else.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_iterate(aksl_HashMap *map,
aksl_HashMapIterator iter,
void *data)
{
size_t i = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, iter=%p", (void *)map, (void *)iter);
FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "map=%p, iter=%p", (void *)map, (void *)iter);
FAIL_ZERO_RETURN(e, map->slots, AKERR_NULLPOINTER, "map is not initialised");
for ( i = 0; i < map->capacity; i++ ) {
if ( map->slots[i].state != AKSL_HASHMAP_SLOT_OCCUPIED ) {
continue;
}
ATTEMPT {
CATCH(e, iter(map->slots[i].key, map->slots[i].value, data));
} CLEANUP {
} PROCESS(e) {
} HANDLE(e, AKERR_ITERATOR_BREAK) {
// Not an error condition; it is telling us to stop early
SUCCEED_RETURN(e);
} FINISH(e, true);
}
SUCCEED_RETURN(e);
}
/* ====================================================================== */
/* Growable string buffer */
/* ====================================================================== */
/*
* The one thing here that owns memory.
*
* The bounded formatting wrappers are the right answer when the destination is
* a fixed buffer, and no answer at all when the output length is not known in
* advance -- which is why the collections plan asked for this to "make the snprintf and
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
* strcat wrappers pleasant to use". Building a diagnostic, a serialised record
* or a generated line means appending to something that grows.
*
* Capacity doubles, so n appends cost O(n) amortised rather than O(n^2). The
* buffer is always NUL-terminated, so aksl_strbuf_cstr is valid at any point
* without a finalise step -- and the terminator is not counted in `length`, so
* embedded NULs are possible but the C string view stops at the first one.
*/
#define AKSL_STRBUF_MIN_CAPACITY 32
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_init(aksl_StrBuf *buf, size_t initial)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf");
buf->data = NULL;
buf->length = 0;
buf->capacity = 0;
if ( initial < AKSL_STRBUF_MIN_CAPACITY ) {
initial = AKSL_STRBUF_MIN_CAPACITY;
}
ATTEMPT {
CATCH(e, aksl_malloc(initial, (void **)&buf->data));
buf->capacity = initial;
buf->data[0] = '\0';
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/* Make room for `extra` more bytes plus the terminator, doubling as needed. */
static akerr_ErrorContext AKERR_NOIGNORE *strbuf_reserve(aksl_StrBuf *buf, size_t extra)
{
size_t needed = 0;
size_t capacity = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf->data, AKERR_NULLPOINTER, "buffer is not initialised");
/* size_t overflow would turn a huge append into a tiny allocation. */
FAIL_NONZERO_RETURN(e, (extra > (size_t)-1 - buf->length - 1), AKERR_OUTOFBOUNDS,
"appending %zu bytes to %zu overflows size_t", extra, buf->length);
needed = buf->length + extra + 1;
if ( needed <= buf->capacity ) {
SUCCEED_RETURN(e);
}
capacity = buf->capacity;
while ( capacity < needed ) {
/* Doubling, but never past what size_t can hold. */
if ( capacity > ((size_t)-1) / 2 ) {
capacity = needed;
break;
}
capacity *= 2;
}
ATTEMPT {
CATCH(e, aksl_realloc((void **)&buf->data, capacity));
buf->capacity = capacity;
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append(aksl_StrBuf *buf, const char *s)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p, s=%p", (void *)buf, (void *)s);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "buf=%p, s=%p", (void *)buf, (void *)s);
return aksl_strbuf_append_bytes(buf, s, strlen(s));
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_bytes(aksl_StrBuf *buf, const char *s, size_t n)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p, s=%p", (void *)buf, (void *)s);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "buf=%p, s=%p", (void *)buf, (void *)s);
ATTEMPT {
CATCH(e, strbuf_reserve(buf, n));
memcpy(buf->data + buf->length, s, n);
buf->length += n;
buf->data[buf->length] = '\0';
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_char(aksl_StrBuf *buf, char c)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf");
return aksl_strbuf_append_bytes(buf, &c, 1);
}
/*
* Formatted append. vsnprintf is called twice on purpose: once with a zero
* length to ask how much room the result needs, and again once that room
* exists. The alternative -- guess, then retry on truncation -- is the same two
* calls in the bad case and a wasted guess in the good one.
*
* The va_list is copied because a vsnprintf leaves it consumed, and using the
* same one twice is undefined behaviour rather than merely wrong.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_vappendf(aksl_StrBuf *buf, const char *format, va_list args)
{
va_list measure;
int needed = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p, format=%p", (void *)buf, (void *)format);
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "buf=%p, format=%p", (void *)buf, (void *)format);
va_copy(measure, args);
needed = vsnprintf(NULL, 0, format, measure);
va_end(measure);
FAIL_NONZERO_RETURN(e, (needed < 0), AKERR_IO, "could not format the arguments");
ATTEMPT {
CATCH(e, strbuf_reserve(buf, (size_t)needed));
/*
* The reserve above guarantees needed + 1 bytes, so this cannot
* truncate -- vsnprintf's return is checked all the same, because
* "cannot happen" is a claim about today's reserve.
*/
FAIL_NONZERO_BREAK(e,
(vsnprintf(buf->data + buf->length,
buf->capacity - buf->length, format, args) != needed),
AKERR_IO, "formatted output changed length between passes");
buf->length += (size_t)needed;
buf->data[buf->length] = '\0';
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_appendf(aksl_StrBuf *buf, const char *format, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, format);
raised = aksl_strbuf_vappendf(buf, format, args);
va_end(args);
return raised;
}
/* Empties without releasing, so the capacity is reused by the next round. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_reset(aksl_StrBuf *buf)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf");
FAIL_ZERO_RETURN(e, buf->data, AKERR_NULLPOINTER, "buffer is not initialised");
buf->length = 0;
buf->data[0] = '\0';
SUCCEED_RETURN(e);
}
/*
* The contents as a C string. Points into the buffer, so it is invalidated by
* the next append -- copy it with aksl_strdup if it has to outlive one.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_cstr(aksl_StrBuf *buf, const char **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p, dest=%p", (void *)buf, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "buf=%p, dest=%p", (void *)buf, (void *)dest);
FAIL_ZERO_RETURN(e, buf->data, AKERR_NULLPOINTER, "buffer is not initialised");
*dest = buf->data;
SUCCEED_RETURN(e);
}
/*
* Releases the storage and leaves the buffer in the state aksl_strbuf_init would
* refuse to leave it in -- zeroed, so a second free is an error rather than a
* double free, exactly as aksl_freep arranges for a bare pointer.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_free(aksl_StrBuf *buf)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf");
FAIL_ZERO_RETURN(e, buf->data, AKERR_NULLPOINTER, "buffer is already released");
ATTEMPT {
CATCH(e, aksl_free(buf->data));
} CLEANUP {
buf->data = NULL;
buf->length = 0;
buf->capacity = 0;
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}