Whitespace only -- `git diff -w` is empty. TODO.md 2.3 recorded that the
tree mixed hard tabs and space indents within the same functions;
tests/aksl_capture.h was the worst of it, with the assertion macros
space-indented and everything around them tabbed.
The style is the one libakerror standardised on and the one AGENTS.md
already describes: Emacs cc-mode "stroustrup", c-basic-offset 4,
indent-tabs-mode on, tab-width 8. A correct file is a fixed point of
indent-region under those settings, and the tree is one now -- a second
pass produces no diff.
scripts/reindent.el is that pass, checked in so "correct" is something
you can run rather than something you have to remember. It is not
clang-format and deliberately so: AGENTS.md forbids introducing one
without discussion, and this is a good illustration of why. cc-mode
indents every declaration in akstdlib.h one level for the extern "C" { }
wrapping the file body, so the script sets inextern-lang to 0 -- a
cc-mode offset with no clang-format equivalent, and without it the first
run moved 364 lines in the wrong direction.
Its own commit, no behaviour change, and the suite is green either side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
1131 lines
39 KiB
C
1131 lines
39 KiB
C
/*
|
|
* Data structures -- TODO.md section 3.6.
|
|
*
|
|
* 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)
|
|
{
|
|
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
|
|
* TODO.md 3.6 means by "a head/tail-tracking container type so append is O(1)".
|
|
*
|
|
* 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 (TODO.md 2.2.15).
|
|
* 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 TODO.md 3.6's hash request. It differs from djb2 in
|
|
* 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 TODO.md 3.6 says is "worth lifting
|
|
* 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 TODO.md 3.6 asks for this to "make the snprintf and
|
|
* 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);
|
|
}
|