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>
2026-07-31 07:32:35 -04:00
|
|
|
/*
|
|
|
|
|
* List and tree additions -- src/collections.c, TODO.md section 3.6.
|
|
|
|
|
*
|
|
|
|
|
* The bare-node list functions, the tracked aksl_List container, and the binary
|
|
|
|
|
* search tree. The hash map and string buffer have their own files.
|
|
|
|
|
*
|
|
|
|
|
* Nodes here are stack arrays, which is the point: none of these functions
|
|
|
|
|
* allocates, so a caller drawing from a fixed pool can use all of them. The two
|
|
|
|
|
* *_free_all tests use aksl_malloc explicitly, because releasing is the one
|
|
|
|
|
* thing that has to know where the memory came from.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
|
|
|
|
|
|
#define N 5
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
/* Helpers */
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
typedef struct VisitLog
|
|
|
|
|
{
|
|
|
|
|
int count;
|
|
|
|
|
aksl_ListNode *seen[16];
|
|
|
|
|
int break_at;
|
|
|
|
|
} VisitLog;
|
|
|
|
|
|
|
|
|
|
static void visitlog_init(VisitLog *log)
|
|
|
|
|
{
|
|
|
|
|
memset((void *)log, 0x00, sizeof(VisitLog));
|
|
|
|
|
log->break_at = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void *data)
|
|
|
|
|
{
|
|
|
|
|
VisitLog *log = NULL;
|
|
|
|
|
int idx = 0;
|
|
|
|
|
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
|
|
|
|
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
|
|
|
|
log = (VisitLog *)data;
|
|
|
|
|
idx = log->count;
|
|
|
|
|
if ( idx < 16 ) {
|
|
|
|
|
log->seen[idx] = node;
|
|
|
|
|
}
|
|
|
|
|
log->count += 1;
|
|
|
|
|
if ( log->break_at == idx ) {
|
|
|
|
|
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop at visit %d", idx);
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Accepts the node whose data pointer equals `data`. */
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *match_data(aksl_ListNode *node, void *data, int *matched)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
|
|
|
|
FAIL_ZERO_RETURN(e, matched, AKERR_NULLPOINTER, "matched");
|
|
|
|
|
*matched = (node->data == data) ? 1 : 0;
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* A predicate that fails, to prove the error comes back out of the search. */
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *failing_predicate(aksl_ListNode *node, void *data, int *matched)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
(void)node;
|
|
|
|
|
(void)data;
|
|
|
|
|
(void)matched;
|
|
|
|
|
FAIL_RETURN(e, AKERR_VALUE, "predicate refused to answer");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Build node[0..n) into a chain and hand back the head. */
|
|
|
|
|
static void build_chain(aksl_ListNode *node, int n)
|
|
|
|
|
{
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < n; i++ ) {
|
|
|
|
|
memset((void *)&node[i], 0x00, sizeof(aksl_ListNode));
|
|
|
|
|
}
|
|
|
|
|
for ( i = 1; i < n; i++ ) {
|
|
|
|
|
node[i - 1].next = &node[i];
|
|
|
|
|
node[i].prev = &node[i - 1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
/* Insertion */
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
static int test_prepend_moves_the_head(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode node[3];
|
|
|
|
|
aksl_ListNode *head = NULL;
|
|
|
|
|
|
|
|
|
|
build_chain(node, 3);
|
|
|
|
|
head = &node[1];
|
|
|
|
|
node[1].prev = NULL;
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_prepend(&head, &node[0]));
|
|
|
|
|
AKSL_CHECK(head == &node[0]);
|
|
|
|
|
AKSL_CHECK(node[0].next == &node[1]);
|
|
|
|
|
AKSL_CHECK(node[0].prev == NULL);
|
|
|
|
|
AKSL_CHECK(node[1].prev == &node[0]);
|
|
|
|
|
|
|
|
|
|
/* Prepending onto an empty list makes a one-node list. */
|
|
|
|
|
head = NULL;
|
|
|
|
|
memset((void *)&node[2], 0x00, sizeof(node[2]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_prepend(&head, &node[2]));
|
|
|
|
|
AKSL_CHECK(head == &node[2]);
|
|
|
|
|
AKSL_CHECK(node[2].next == NULL);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_prepend(&head, &node[2]), AKERR_VALUE);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node[0]), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_prepend(&head, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_insert_after_and_before(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode node[4];
|
|
|
|
|
aksl_ListNode *head = &node[0];
|
|
|
|
|
|
|
|
|
|
build_chain(node, 2);
|
|
|
|
|
memset((void *)&node[2], 0x00, sizeof(node[2]));
|
|
|
|
|
memset((void *)&node[3], 0x00, sizeof(node[3]));
|
|
|
|
|
|
|
|
|
|
/* node[0] -> node[2] -> node[1] */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_insert_after(&node[0], &node[2]));
|
|
|
|
|
AKSL_CHECK(node[0].next == &node[2]);
|
|
|
|
|
AKSL_CHECK(node[2].prev == &node[0]);
|
|
|
|
|
AKSL_CHECK(node[2].next == &node[1]);
|
|
|
|
|
AKSL_CHECK(node[1].prev == &node[2]);
|
|
|
|
|
|
|
|
|
|
/* Inserting before the head moves it, which is why head is required. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_insert_before(&head, &node[0], &node[3]));
|
|
|
|
|
AKSL_CHECK(head == &node[3]);
|
|
|
|
|
AKSL_CHECK(node[3].next == &node[0]);
|
|
|
|
|
AKSL_CHECK(node[0].prev == &node[3]);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_insert_after(&node[0], &node[0]), AKERR_VALUE);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_insert_after(NULL, &node[0]), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_insert_after(&node[0], NULL), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_insert_before(&head, &node[0], &node[0]), AKERR_VALUE);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_insert_before(NULL, &node[0], &node[1]), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_insert_before(&head, NULL, &node[1]), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_insert_before(&head, &node[0], NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
|
|
|
/*
|
|
|
|
|
* Inserting before a node that is *not* the head, which relinks the node in
|
|
|
|
|
* front of it as well -- a different path from inserting before the head, where
|
|
|
|
|
* there is no such node.
|
|
|
|
|
*/
|
|
|
|
|
static int test_insert_before_a_middle_node(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode node[3];
|
|
|
|
|
aksl_ListNode fresh;
|
|
|
|
|
aksl_ListNode *head = &node[0];
|
|
|
|
|
|
|
|
|
|
build_chain(node, 3);
|
|
|
|
|
memset((void *)&fresh, 0x00, sizeof(fresh));
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_insert_before(&head, &node[2], &fresh));
|
|
|
|
|
AKSL_CHECK(head == &node[0]); /* unchanged, unlike the head case */
|
|
|
|
|
AKSL_CHECK(node[1].next == &fresh);
|
|
|
|
|
AKSL_CHECK(fresh.prev == &node[1]);
|
|
|
|
|
AKSL_CHECK(fresh.next == &node[2]);
|
|
|
|
|
AKSL_CHECK(node[2].prev == &fresh);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-07-31 07:32:35 -04:00
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
/* Inspection */
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
static int test_length_counts_and_refuses_cycles(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode node[N];
|
|
|
|
|
size_t n = 99;
|
|
|
|
|
|
|
|
|
|
/* An empty list is length 0, not an error. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_length(NULL, &n));
|
|
|
|
|
AKSL_CHECK(n == 0);
|
|
|
|
|
|
|
|
|
|
build_chain(node, N);
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_length(&node[0], &n));
|
|
|
|
|
AKSL_CHECK(n == N);
|
|
|
|
|
|
|
|
|
|
node[N - 1].next = &node[0];
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_length(&node[0], &n), AKERR_CIRCULAR_REFERENCE);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_length(&node[0], NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_find_returns_the_first_match_or_null(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode node[N];
|
|
|
|
|
aksl_ListNode *found = (aksl_ListNode *)0x1;
|
Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
|
|
|
aksl_ListNode *cyclic = NULL;
|
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>
2026-07-31 07:32:35 -04:00
|
|
|
int payload = 42;
|
|
|
|
|
int absent = 0;
|
|
|
|
|
|
|
|
|
|
build_chain(node, N);
|
|
|
|
|
node[2].data = &payload;
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_find(&node[0], &match_data, &payload, &found));
|
|
|
|
|
AKSL_CHECK(found == &node[2]);
|
|
|
|
|
|
|
|
|
|
/* Nothing matches: NULL and success, not an error. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_find(&node[0], &match_data, &absent, &found));
|
|
|
|
|
AKSL_CHECK(found == NULL);
|
|
|
|
|
|
|
|
|
|
/* An empty list finds nothing, equally without complaint. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_find(NULL, &match_data, &payload, &found));
|
|
|
|
|
AKSL_CHECK(found == NULL);
|
|
|
|
|
|
|
|
|
|
/* A predicate that raises stops the search and propagates. */
|
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
|
|
|
|
aksl_list_find(&node[0], &failing_predicate, NULL, &found),
|
|
|
|
|
AKERR_VALUE, "predicate refused");
|
|
|
|
|
|
Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
|
|
|
/*
|
|
|
|
|
* A cyclic list is refused before the walk starts rather than searched
|
|
|
|
|
* forever. Every whole-list function shares one bounded tail walk, so this
|
|
|
|
|
* covers the bound for find, reverse, concat and free_all alike.
|
|
|
|
|
*/
|
|
|
|
|
node[N - 1].next = &node[0];
|
|
|
|
|
cyclic = &node[0];
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, &payload, &found),
|
|
|
|
|
AKERR_CIRCULAR_REFERENCE);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_reverse(&cyclic), AKERR_CIRCULAR_REFERENCE);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_concat(&node[0], &node[2]), AKERR_CIRCULAR_REFERENCE);
|
|
|
|
|
node[N - 1].next = NULL;
|
|
|
|
|
|
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>
2026-07-31 07:32:35 -04:00
|
|
|
AKSL_CHECK_STATUS(aksl_list_find(&node[0], NULL, NULL, &found), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, NULL, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
/* Rearranging */
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
static int test_reverse_flips_both_directions(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode node[N];
|
|
|
|
|
aksl_ListNode *head = NULL;
|
|
|
|
|
aksl_ListNode *walk = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
build_chain(node, N);
|
|
|
|
|
head = &node[0];
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_reverse(&head));
|
|
|
|
|
AKSL_CHECK(head == &node[N - 1]);
|
|
|
|
|
|
|
|
|
|
/* Forwards through the reversed list is backwards through the array. */
|
|
|
|
|
walk = head;
|
|
|
|
|
for ( i = N - 1; i >= 0; i-- ) {
|
|
|
|
|
AKSL_CHECK(walk == &node[i]);
|
|
|
|
|
walk = walk->next;
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK(walk == NULL);
|
|
|
|
|
|
|
|
|
|
/* And the prev links were flipped too, not just the next ones. */
|
|
|
|
|
walk = &node[0];
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
AKSL_CHECK(walk == &node[i]);
|
|
|
|
|
walk = walk->prev;
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK(walk == NULL);
|
|
|
|
|
|
|
|
|
|
/* Reversing an empty list is a no-op, not an error. */
|
|
|
|
|
head = NULL;
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_reverse(&head));
|
|
|
|
|
AKSL_CHECK(head == NULL);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_reverse(NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_concat_joins_two_lists(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode first[3];
|
|
|
|
|
aksl_ListNode second[2];
|
|
|
|
|
size_t n = 0;
|
|
|
|
|
|
|
|
|
|
build_chain(first, 3);
|
|
|
|
|
build_chain(second, 2);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_concat(&first[0], &second[0]));
|
|
|
|
|
AKSL_CHECK(first[2].next == &second[0]);
|
|
|
|
|
AKSL_CHECK(second[0].prev == &first[2]);
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_length(&first[0], &n));
|
|
|
|
|
AKSL_CHECK(n == 5);
|
|
|
|
|
|
|
|
|
|
/* Concatenating with NULL is a no-op; with itself would make a cycle. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_concat(&first[0], NULL));
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_concat(&first[0], &first[0]), AKERR_VALUE);
|
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_concat(&first[0], &second[1]),
|
|
|
|
|
AKERR_VALUE, "already in the destination");
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_concat(NULL, &second[0]), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
/* Reverse iteration */
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
static int test_iterate_reverse_walks_back_to_the_head(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode node[N];
|
|
|
|
|
VisitLog log;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
build_chain(node, N);
|
|
|
|
|
visitlog_init(&log);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log));
|
|
|
|
|
AKSL_CHECK(log.count == N);
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
AKSL_CHECK(log.seen[i] == &node[N - 1 - i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* The break works going this way too. */
|
|
|
|
|
visitlog_init(&log);
|
|
|
|
|
log.break_at = 1;
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log));
|
|
|
|
|
AKSL_CHECK(log.count == 2);
|
|
|
|
|
|
|
|
|
|
/* And a cycle in the prev links is refused, as it is in the next links. */
|
|
|
|
|
node[0].prev = &node[N - 1];
|
|
|
|
|
visitlog_init(&log);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log),
|
|
|
|
|
AKERR_CIRCULAR_REFERENCE);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_iterate_reverse(NULL, &record_visit, &log), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_iterate_reverse(&node[0], NULL, &log), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
/* Releasing */
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
static int test_free_all_releases_every_node(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode *head = NULL;
|
|
|
|
|
aksl_ListNode *node = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
/* A heap list, since this is the one operation that has to own its memory. */
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
|
|
|
|
|
if ( head == NULL ) {
|
|
|
|
|
head = node;
|
|
|
|
|
} else {
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_append(head, node));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_free_all(&head, NULL));
|
|
|
|
|
AKSL_CHECK(head == NULL);
|
|
|
|
|
|
|
|
|
|
/* An empty list frees cleanly, and a second call is a no-op rather than a
|
|
|
|
|
* double free, because the head was cleared. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_free_all(&head, NULL));
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_free_all(NULL, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
|
|
|
/*
|
|
|
|
|
* A free function that fails on one node in the middle.
|
|
|
|
|
*
|
|
|
|
|
* Both *_free_all functions go out of their way to keep the first error and
|
|
|
|
|
* carry on rather than returning immediately, because abandoning the walk would
|
|
|
|
|
* leak everything after the node that failed -- turning one bad free into a leak
|
|
|
|
|
* of the whole remaining structure. This is the test that says so.
|
|
|
|
|
*/
|
|
|
|
|
/* Defined with the tree tests below; declared here because the tree free-all
|
|
|
|
|
* case belongs beside the list one rather than beside its comparator. */
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *compare_ints(void *a, void *b, int *dest);
|
|
|
|
|
|
|
|
|
|
static int failing_free_countdown = 0;
|
|
|
|
|
static int failing_free_calls = 0;
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_once(void *ptr)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
failing_free_calls += 1;
|
|
|
|
|
if ( failing_free_calls == failing_free_countdown ) {
|
|
|
|
|
/* Refuse, but still release the memory: the point is the error path,
|
|
|
|
|
* not a deliberate leak for the sanitizer to find. */
|
|
|
|
|
free(ptr);
|
|
|
|
|
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
|
|
|
|
}
|
|
|
|
|
free(ptr);
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* And one that refuses every time, so more than one error is raised during a
|
|
|
|
|
* single walk. Only the first is kept and handed back; the rest have to be
|
|
|
|
|
* released, or a walk over n nodes with a broken free would consume n pool slots
|
|
|
|
|
* and exhaust the pool -- the failure mode the whole pool-accounting section of
|
|
|
|
|
* TODO.md 1.9 exists to catch.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *free_that_always_fails(void *ptr)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
failing_free_calls += 1;
|
|
|
|
|
free(ptr);
|
|
|
|
|
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* And one that refuses from a given call onwards, which is what it takes to fail
|
|
|
|
|
* repeatedly *inside* a drain: the dequeues that come first have to succeed, or
|
|
|
|
|
* the walk stops with a real error before it ever reaches the break.
|
|
|
|
|
*/
|
|
|
|
|
static int failing_free_from = 0;
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_from(void *ptr)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
failing_free_calls += 1;
|
|
|
|
|
free(ptr);
|
|
|
|
|
if ( failing_free_calls >= failing_free_from ) {
|
|
|
|
|
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_free_all_releases_the_errors_it_does_not_return(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode *head = NULL;
|
|
|
|
|
aksl_ListNode *node = NULL;
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
aksl_TreeNode *tnode = NULL;
|
|
|
|
|
static int values[N] = { 5, 3, 8, 1, 9 };
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
|
|
|
|
|
if ( head == NULL ) {
|
|
|
|
|
head = node;
|
|
|
|
|
} else {
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_append(head, node));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
failing_free_calls = 0;
|
|
|
|
|
/* The first of N errors comes back; the other N-1 must not leak. */
|
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_free_all(&head, &free_that_always_fails),
|
|
|
|
|
AKERR_VALUE, "free refused on call 1");
|
|
|
|
|
AKSL_CHECK(failing_free_calls == N);
|
|
|
|
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&tnode));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(tnode, &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, tnode, &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
failing_free_calls = 0;
|
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_tree_free_all(&root, &free_that_always_fails),
|
|
|
|
|
AKERR_VALUE, "free refused on call 1");
|
|
|
|
|
AKSL_CHECK(failing_free_calls == N);
|
|
|
|
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_list_free_all_reports_the_first_failure_but_frees_everything(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_ListNode *head = NULL;
|
|
|
|
|
aksl_ListNode *node = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
|
|
|
|
|
if ( head == NULL ) {
|
|
|
|
|
head = node;
|
|
|
|
|
} else {
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_append(head, node));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
failing_free_calls = 0;
|
|
|
|
|
failing_free_countdown = 2; /* fail on the second of five */
|
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_free_all(&head, &free_that_fails_once),
|
|
|
|
|
AKERR_VALUE, "free refused on call 2");
|
|
|
|
|
/* Every node was still visited: the walk did not stop at the failure. */
|
|
|
|
|
AKSL_CHECK(failing_free_calls == N);
|
|
|
|
|
AKSL_CHECK(head == NULL);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_tree_free_all_reports_the_first_failure_but_frees_everything(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[5] = { 5, 3, 8, 1, 9 };
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
aksl_TreeNode *node = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < 5; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&node));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(node, &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, node, &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
failing_free_calls = 0;
|
|
|
|
|
failing_free_countdown = 2;
|
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_tree_free_all(&root, &free_that_fails_once),
|
|
|
|
|
AKERR_VALUE, "free refused on call 2");
|
|
|
|
|
AKSL_CHECK(failing_free_calls == 5);
|
|
|
|
|
AKSL_CHECK(root == NULL);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The same idea for the breadth-first traversal queue: a failing lfree during
|
|
|
|
|
* the drain must not stop the drain, and must not mask the error that got the
|
|
|
|
|
* walk there in the first place. The traversal itself still succeeds -- the
|
|
|
|
|
* drain failure is logged and dropped, because losing the caller's real error to
|
|
|
|
|
* report a bookkeeping one would be the worse trade.
|
|
|
|
|
*/
|
|
|
|
|
static aksl_TreeNode *break_on_node = NULL;
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *break_at(aksl_TreeNode *node, void *data)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
(void)data;
|
|
|
|
|
if ( node == break_on_node ) {
|
|
|
|
|
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop here");
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *plain_alloc(size_t size, void **dest)
|
|
|
|
|
{
|
|
|
|
|
return aksl_malloc(size, dest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_bfs_queue_drain_survives_a_failing_free(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_TreeNode tree[3];
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < 3; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
|
|
|
|
|
}
|
|
|
|
|
tree[0].left = &tree[1];
|
|
|
|
|
tree[0].right = &tree[2];
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The lfree calls, in order:
|
|
|
|
|
* 1 the root's queue entry, released as it is dequeued
|
|
|
|
|
* 2 the left child's entry, likewise
|
|
|
|
|
* -- the callback breaks on the left child, leaving the right child queued
|
|
|
|
|
* 3 the right child's entry, released by the drain in CLEANUP
|
|
|
|
|
*
|
|
|
|
|
* Failing on call 3 is therefore a failure inside the drain. The traversal
|
|
|
|
|
* still succeeds: the break is not an error, and losing that answer in order
|
|
|
|
|
* to report a bookkeeping failure would be the worse trade -- so the drain
|
|
|
|
|
* error is logged and dropped, which is what this asserts.
|
|
|
|
|
*/
|
|
|
|
|
break_on_node = &tree[1];
|
|
|
|
|
failing_free_calls = 0;
|
|
|
|
|
failing_free_countdown = 3;
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at,
|
|
|
|
|
&plain_alloc, &free_that_fails_once,
|
|
|
|
|
AKSL_TREE_SEARCH_BFS, NULL));
|
|
|
|
|
AKSL_CHECK(failing_free_calls == 3);
|
|
|
|
|
|
|
|
|
|
/* And the pool is intact afterwards: the dropped context was released. */
|
|
|
|
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* More than one failure inside a single drain, which is the case where the drain
|
|
|
|
|
* has to release the errors it is not keeping. A seven-node tree broken on the
|
|
|
|
|
* third visit leaves two entries queued:
|
|
|
|
|
*
|
|
|
|
|
* dequeue 0 (free 1), enqueue 1 and 2
|
|
|
|
|
* dequeue 1 (free 2), enqueue 3 and 4 queue: 2 3 4
|
|
|
|
|
* dequeue 2 (free 3), callback breaks queue: 3 4
|
|
|
|
|
* drain frees 3 and 4 (free 4, free 5)
|
|
|
|
|
*
|
|
|
|
|
* With a free that refuses every time, the drain raises twice and must return at
|
|
|
|
|
* most one context to be dropped -- keeping both would leak a pool slot per
|
|
|
|
|
* queued node, which over a large tree exhausts the pool outright.
|
|
|
|
|
*/
|
|
|
|
|
static int test_bfs_queue_drain_releases_repeated_failures(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_TreeNode tree[7];
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < 7; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
|
|
|
|
|
}
|
|
|
|
|
tree[0].left = &tree[1];
|
|
|
|
|
tree[0].right = &tree[2];
|
|
|
|
|
tree[1].left = &tree[3];
|
|
|
|
|
tree[1].right = &tree[4];
|
|
|
|
|
tree[2].left = &tree[5];
|
|
|
|
|
tree[2].right = &tree[6];
|
|
|
|
|
|
|
|
|
|
break_on_node = &tree[2];
|
|
|
|
|
failing_free_calls = 0;
|
|
|
|
|
failing_free_from = 4; /* the first drain call */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at,
|
|
|
|
|
&plain_alloc, &free_that_fails_from,
|
|
|
|
|
AKSL_TREE_SEARCH_BFS, NULL));
|
|
|
|
|
/* Three dequeues plus two drained entries. */
|
|
|
|
|
AKSL_CHECK(failing_free_calls == 5);
|
|
|
|
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-07-31 07:32:35 -04:00
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
/* The tracked container */
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The reason the container exists: aksl_list_append walks to the tail every
|
|
|
|
|
* time, so building n nodes with it is O(n^2). push is O(1) and the length is a
|
|
|
|
|
* field rather than a walk.
|
|
|
|
|
*/
|
|
|
|
|
static int test_container_push_and_unshift(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_List list;
|
|
|
|
|
aksl_ListNode node[N];
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_init(&list));
|
|
|
|
|
AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0);
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
memset((void *)&node[i], 0x00, sizeof(node[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_push(&list, &node[i]));
|
|
|
|
|
AKSL_CHECK(list.length == (size_t)(i + 1));
|
|
|
|
|
AKSL_CHECK(list.tail == &node[i]);
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK(list.head == &node[0]);
|
|
|
|
|
AKSL_CHECK(node[0].prev == NULL);
|
|
|
|
|
AKSL_CHECK(node[N - 1].next == NULL);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_init(&list));
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
memset((void *)&node[i], 0x00, sizeof(node[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_unshift(&list, &node[i]));
|
|
|
|
|
AKSL_CHECK(list.head == &node[i]);
|
|
|
|
|
AKSL_CHECK(list.tail == &node[0]);
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK(list.length == N);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_init(NULL), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_push(NULL, &node[0]), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_push(&list, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_unshift(NULL, &node[0]), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_unshift(&list, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Removing keeps head, tail and length describing the list. */
|
|
|
|
|
static int test_container_remove_maintains_the_endpoints(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_List list;
|
|
|
|
|
aksl_ListNode node[3];
|
|
|
|
|
aksl_ListNode stranger;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_init(&list));
|
|
|
|
|
for ( i = 0; i < 3; i++ ) {
|
|
|
|
|
memset((void *)&node[i], 0x00, sizeof(node[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_push(&list, &node[i]));
|
|
|
|
|
}
|
|
|
|
|
memset((void *)&stranger, 0x00, sizeof(stranger));
|
|
|
|
|
|
|
|
|
|
/* Middle. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_remove(&list, &node[1]));
|
|
|
|
|
AKSL_CHECK(list.length == 2);
|
|
|
|
|
AKSL_CHECK(list.head == &node[0] && list.tail == &node[2]);
|
|
|
|
|
AKSL_CHECK(node[0].next == &node[2] && node[2].prev == &node[0]);
|
|
|
|
|
|
|
|
|
|
/* Head. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_remove(&list, &node[0]));
|
|
|
|
|
AKSL_CHECK(list.head == &node[2] && list.tail == &node[2]);
|
|
|
|
|
AKSL_CHECK(list.length == 1);
|
|
|
|
|
|
|
|
|
|
/* Last one out empties both endpoints. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_remove(&list, &node[2]));
|
|
|
|
|
AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0);
|
|
|
|
|
|
|
|
|
|
/* A node that is not in this list is refused rather than corrupting it. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_push(&list, &node[0]));
|
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_remove(&list, &stranger),
|
|
|
|
|
AKERR_VALUE, "not in this list");
|
|
|
|
|
AKSL_CHECK(list.length == 1);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_remove(NULL, &node[0]), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_remove(&list, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_container_clear(void)
|
|
|
|
|
{
|
|
|
|
|
aksl_List list;
|
|
|
|
|
aksl_ListNode *node = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_init(&list));
|
|
|
|
|
for ( i = 0; i < N; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_push(&list, node));
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK(list.length == N);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_list_clear(&list, NULL));
|
|
|
|
|
AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_list_clear(NULL, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
/* Binary search tree */
|
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
/* Compares the ints the leaf pointers point at. */
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *compare_ints(void *a, void *b, int *dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a");
|
|
|
|
|
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "b");
|
|
|
|
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
|
|
|
|
|
*dest = *(int *)a - *(int *)b;
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typedef struct OrderLog
|
|
|
|
|
{
|
|
|
|
|
int count;
|
|
|
|
|
int seen[16];
|
|
|
|
|
} OrderLog;
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *record_leaf(aksl_TreeNode *node, void *data)
|
|
|
|
|
{
|
|
|
|
|
OrderLog *log = NULL;
|
|
|
|
|
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
|
|
|
|
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
|
|
|
|
log = (OrderLog *)data;
|
|
|
|
|
if ( log->count < 16 ) {
|
|
|
|
|
log->seen[log->count] = *(int *)node->leaf;
|
|
|
|
|
}
|
|
|
|
|
log->count += 1;
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Insertion order 5 3 8 1 4 7 9 builds a tree whose in-order walk is sorted --
|
|
|
|
|
* which is the whole invariant a search tree exists to maintain, so asserting it
|
|
|
|
|
* is worth more than asserting any particular shape.
|
|
|
|
|
*/
|
|
|
|
|
static int test_tree_insert_orders_the_leaves(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[7] = { 5, 3, 8, 1, 4, 7, 9 };
|
|
|
|
|
aksl_TreeNode node[7];
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
OrderLog log;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < 7; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK(root == &node[0]);
|
|
|
|
|
|
|
|
|
|
memset((void *)&log, 0x00, sizeof(log));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL,
|
|
|
|
|
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
|
|
|
|
AKSL_CHECK(log.count == 7);
|
|
|
|
|
for ( i = 1; i < log.count; i++ ) {
|
|
|
|
|
AKSL_CHECK(log.seen[i - 1] < log.seen[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_insert(NULL, &node[0], &compare_ints), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_insert(&root, NULL, &compare_ints), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_insert(&root, &node[0], NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* TODO.md 2.2.15: aksl_TreeNode.parent was declared and never touched by
|
|
|
|
|
* anything in the library. These are the functions that set it, and
|
|
|
|
|
* aksl_tree_remove is the one that needs it.
|
|
|
|
|
*/
|
|
|
|
|
static int test_tree_insert_sets_the_parent_links(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[3] = { 5, 3, 8 };
|
|
|
|
|
aksl_TreeNode node[3];
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < 3; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK(node[0].parent == NULL); /* the root */
|
|
|
|
|
AKSL_CHECK(node[1].parent == &node[0]);
|
|
|
|
|
AKSL_CHECK(node[2].parent == &node[0]);
|
|
|
|
|
AKSL_CHECK(node[0].left == &node[1]);
|
|
|
|
|
AKSL_CHECK(node[0].right == &node[2]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_tree_find(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[5] = { 5, 3, 8, 1, 9 };
|
|
|
|
|
int wanted = 8;
|
|
|
|
|
int absent = 6;
|
|
|
|
|
aksl_TreeNode node[5];
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
aksl_TreeNode *found = (aksl_TreeNode *)0x1;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < 5; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_find(root, &wanted, &compare_ints, &found));
|
|
|
|
|
AKSL_CHECK(found == &node[2]);
|
|
|
|
|
|
|
|
|
|
/* Absent is NULL and success. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_find(root, &absent, &compare_ints, &found));
|
|
|
|
|
AKSL_CHECK(found == NULL);
|
|
|
|
|
|
|
|
|
|
/* An empty tree finds nothing, equally without complaint. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_find(NULL, &wanted, &compare_ints, &found));
|
|
|
|
|
AKSL_CHECK(found == NULL);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_find(root, &wanted, NULL, &found), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_find(root, &wanted, &compare_ints, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* All three removal cases, each checked by re-walking the tree and confirming it
|
|
|
|
|
* is still sorted and one node shorter. The two-child case is the interesting
|
|
|
|
|
* one: the in-order successor takes the node's place, which is the only value
|
|
|
|
|
* that keeps the ordering invariant on both sides.
|
|
|
|
|
*/
|
|
|
|
|
static int test_tree_remove_all_three_cases(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[7] = { 5, 3, 8, 1, 4, 7, 9 };
|
|
|
|
|
aksl_TreeNode node[7];
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
OrderLog log;
|
|
|
|
|
size_t count = 0;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < 7; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Leaf: node[3] holds 1 and has no children. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[3]));
|
|
|
|
|
AKSL_CHECK(node[3].parent == NULL && node[3].left == NULL && node[3].right == NULL);
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
|
|
|
|
AKSL_CHECK(count == 6);
|
|
|
|
|
|
|
|
|
|
/* One child: node[1] holds 3 and now has only its right child (4). */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[1]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
|
|
|
|
AKSL_CHECK(count == 5);
|
|
|
|
|
|
|
|
|
|
/* Two children: the root, 5, with 4 on the left and 8 on the right. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[0]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
|
|
|
|
AKSL_CHECK(count == 4);
|
|
|
|
|
AKSL_CHECK(root != &node[0]);
|
|
|
|
|
AKSL_CHECK(root->parent == NULL);
|
|
|
|
|
|
|
|
|
|
/* Still sorted after all of that, which is the invariant that matters. */
|
|
|
|
|
memset((void *)&log, 0x00, sizeof(log));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL,
|
|
|
|
|
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
|
|
|
|
AKSL_CHECK(log.count == 4);
|
|
|
|
|
for ( i = 1; i < log.count; i++ ) {
|
|
|
|
|
AKSL_CHECK(log.seen[i - 1] < log.seen[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_remove(NULL, &node[0]), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_remove(&root, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
|
|
|
/*
|
|
|
|
|
* The mirror images of the cases above: a node that is its parent's *right*
|
|
|
|
|
* child, and a node whose only child is on the left. Both take different
|
|
|
|
|
* branches through tree_replace, and neither is reached by the test above.
|
|
|
|
|
*/
|
|
|
|
|
static int test_tree_remove_mirrored_shapes(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[4] = { 5, 8, 7, 6 };
|
|
|
|
|
aksl_TreeNode node[4];
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
aksl_TreeNode *found = NULL;
|
|
|
|
|
int missing = 8;
|
|
|
|
|
size_t count = 0;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* 5 node[0]
|
|
|
|
|
* \
|
|
|
|
|
* 8 node[1], a right child
|
|
|
|
|
* /
|
|
|
|
|
* 7 node[2], whose only child is on the left
|
|
|
|
|
* /
|
|
|
|
|
* 6 node[3]
|
|
|
|
|
*/
|
|
|
|
|
for ( i = 0; i < 4; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK(node[0].right == &node[1]);
|
|
|
|
|
AKSL_CHECK(node[1].left == &node[2]);
|
|
|
|
|
AKSL_CHECK(node[2].left == &node[3]);
|
|
|
|
|
|
|
|
|
|
/* A right child with a single left child underneath it. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[1]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
|
|
|
|
AKSL_CHECK(count == 3);
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_find(root, &missing, &compare_ints, &found));
|
|
|
|
|
AKSL_CHECK(found == NULL);
|
|
|
|
|
/* 7 took its place as the root's right child. */
|
|
|
|
|
AKSL_CHECK(node[0].right == &node[2]);
|
|
|
|
|
AKSL_CHECK(node[2].parent == &node[0]);
|
|
|
|
|
|
|
|
|
|
/* And now a node whose only child is on the left. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[2]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
|
|
|
|
AKSL_CHECK(count == 2);
|
|
|
|
|
AKSL_CHECK(node[0].right == &node[3]);
|
|
|
|
|
AKSL_CHECK(node[3].parent == &node[0]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The two-children case where the successor is not the removed node's own right
|
|
|
|
|
* child, so the successor has to be lifted out of its position first. The other
|
|
|
|
|
* removal test happens to hit the adjacent-successor path.
|
|
|
|
|
*/
|
|
|
|
|
static int test_tree_remove_with_a_distant_successor(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[5] = { 5, 3, 9, 7, 8 };
|
|
|
|
|
aksl_TreeNode node[5];
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
OrderLog log;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* 5 node[0]
|
|
|
|
|
* / \
|
|
|
|
|
* 3 9 node[1], node[2]
|
|
|
|
|
* /
|
|
|
|
|
* 7 node[3] -- the in-order successor of 5, two levels down
|
|
|
|
|
* \
|
|
|
|
|
* 8 node[4]
|
|
|
|
|
*/
|
|
|
|
|
for ( i = 0; i < 5; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[0]));
|
|
|
|
|
/* 7 becomes the root, and its own right child (8) is not lost. */
|
|
|
|
|
AKSL_CHECK(root == &node[3]);
|
|
|
|
|
AKSL_CHECK(root->parent == NULL);
|
|
|
|
|
|
|
|
|
|
memset((void *)&log, 0x00, sizeof(log));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL,
|
|
|
|
|
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
|
|
|
|
AKSL_CHECK(log.count == 4);
|
|
|
|
|
for ( i = 1; i < log.count; i++ ) {
|
|
|
|
|
AKSL_CHECK(log.seen[i - 1] < log.seen[i]);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-07-31 07:32:35 -04:00
|
|
|
/* Removing the last node empties the tree rather than leaving a dangling root. */
|
|
|
|
|
static int test_tree_remove_the_only_node(void)
|
|
|
|
|
{
|
|
|
|
|
int value = 1;
|
|
|
|
|
aksl_TreeNode node;
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
size_t count = 99;
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node, &value));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node, &compare_ints));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_remove(&root, &node));
|
|
|
|
|
AKSL_CHECK(root == NULL);
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
|
|
|
|
AKSL_CHECK(count == 0);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_tree_height_and_count(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[7] = { 5, 3, 8, 1, 4, 7, 9 };
|
|
|
|
|
static int chain[4] = { 1, 2, 3, 4 };
|
|
|
|
|
aksl_TreeNode node[7];
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
size_t count = 99;
|
|
|
|
|
int height = 99;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
/* Empty. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_height(NULL, &height));
|
|
|
|
|
AKSL_CHECK(height == 0);
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_count(NULL, &count));
|
|
|
|
|
AKSL_CHECK(count == 0);
|
|
|
|
|
|
|
|
|
|
/* Balanced by construction: 7 nodes, 3 levels. */
|
|
|
|
|
for ( i = 0; i < 7; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
|
|
|
|
AKSL_CHECK(count == 7);
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_height(root, &height));
|
|
|
|
|
AKSL_CHECK(height == 3);
|
|
|
|
|
|
|
|
|
|
/* Sorted input gives a degenerate chain: 4 nodes, 4 levels. This is a plain
|
|
|
|
|
* unbalanced BST and does not pretend otherwise. */
|
|
|
|
|
root = NULL;
|
|
|
|
|
for ( i = 0; i < 4; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &chain[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_height(root, &height));
|
|
|
|
|
AKSL_CHECK(height == 4);
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_height(root, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_count(root, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int test_tree_free_all(void)
|
|
|
|
|
{
|
|
|
|
|
static int values[5] = { 5, 3, 8, 1, 9 };
|
|
|
|
|
aksl_TreeNode *root = NULL;
|
|
|
|
|
aksl_TreeNode *node = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < 5; i++ ) {
|
|
|
|
|
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&node));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_node_init(node, &values[i]));
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_insert(&root, node, &compare_ints));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_free_all(&root, NULL));
|
|
|
|
|
AKSL_CHECK(root == NULL);
|
|
|
|
|
|
|
|
|
|
/* An empty tree frees cleanly and a second call is a no-op. */
|
|
|
|
|
AKSL_CHECK_OK(aksl_tree_free_all(&root, NULL));
|
|
|
|
|
AKSL_CHECK_STATUS(aksl_tree_free_all(NULL, NULL), AKERR_NULLPOINTER);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int main(void)
|
|
|
|
|
{
|
|
|
|
|
int failures = 0;
|
|
|
|
|
|
|
|
|
|
akerr_init();
|
|
|
|
|
|
|
|
|
|
AKSL_RUN(failures, test_prepend_moves_the_head);
|
|
|
|
|
AKSL_RUN(failures, test_insert_after_and_before);
|
Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
|
|
|
AKSL_RUN(failures, test_insert_before_a_middle_node);
|
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>
2026-07-31 07:32:35 -04:00
|
|
|
AKSL_RUN(failures, test_length_counts_and_refuses_cycles);
|
|
|
|
|
AKSL_RUN(failures, test_find_returns_the_first_match_or_null);
|
|
|
|
|
AKSL_RUN(failures, test_reverse_flips_both_directions);
|
|
|
|
|
AKSL_RUN(failures, test_concat_joins_two_lists);
|
|
|
|
|
AKSL_RUN(failures, test_iterate_reverse_walks_back_to_the_head);
|
|
|
|
|
AKSL_RUN(failures, test_free_all_releases_every_node);
|
Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
|
|
|
AKSL_RUN(failures, test_free_all_releases_the_errors_it_does_not_return);
|
|
|
|
|
AKSL_RUN(failures, test_list_free_all_reports_the_first_failure_but_frees_everything);
|
|
|
|
|
AKSL_RUN(failures, test_tree_free_all_reports_the_first_failure_but_frees_everything);
|
|
|
|
|
AKSL_RUN(failures, test_bfs_queue_drain_survives_a_failing_free);
|
|
|
|
|
AKSL_RUN(failures, test_bfs_queue_drain_releases_repeated_failures);
|
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>
2026-07-31 07:32:35 -04:00
|
|
|
|
|
|
|
|
AKSL_RUN(failures, test_container_push_and_unshift);
|
|
|
|
|
AKSL_RUN(failures, test_container_remove_maintains_the_endpoints);
|
|
|
|
|
AKSL_RUN(failures, test_container_clear);
|
|
|
|
|
|
|
|
|
|
AKSL_RUN(failures, test_tree_insert_orders_the_leaves);
|
|
|
|
|
AKSL_RUN(failures, test_tree_insert_sets_the_parent_links);
|
|
|
|
|
AKSL_RUN(failures, test_tree_find);
|
|
|
|
|
AKSL_RUN(failures, test_tree_remove_all_three_cases);
|
Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00
|
|
|
AKSL_RUN(failures, test_tree_remove_mirrored_shapes);
|
|
|
|
|
AKSL_RUN(failures, test_tree_remove_with_a_distant_successor);
|
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>
2026-07-31 07:32:35 -04:00
|
|
|
AKSL_RUN(failures, test_tree_remove_the_only_node);
|
|
|
|
|
AKSL_RUN(failures, test_tree_height_and_count);
|
|
|
|
|
AKSL_RUN(failures, test_tree_free_all);
|
|
|
|
|
|
|
|
|
|
AKSL_REPORT(failures);
|
|
|
|
|
}
|