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>
418 lines
14 KiB
C
418 lines
14 KiB
C
/*
|
|
* The fixed-capacity hash map and FNV-1a -- src/collections.c, TODO.md 3.6.
|
|
*
|
|
* "The single most obviously-missing data structure in the library", by the
|
|
* TODO's own account: akbasic needed one three times over -- variables,
|
|
* functions and labels -- and wrote ~130 lines of open-addressed table to get it.
|
|
*
|
|
* The behaviour worth testing hardest is the tombstone. Deleting a key that
|
|
* something else probed past has to leave a marker rather than an empty slot,
|
|
* because an empty slot ends a probe chain and would make the later key
|
|
* unreachable while it is still sitting there.
|
|
*/
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
#define CAP 16
|
|
|
|
typedef struct SeenLog
|
|
{
|
|
int count;
|
|
char keys[CAP][AKSL_HASHMAP_MAX_KEY];
|
|
int break_at;
|
|
} SeenLog;
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *record_entry(const char *key, void *value, void *data)
|
|
{
|
|
SeenLog *log = NULL;
|
|
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "key");
|
|
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
|
(void)value;
|
|
log = (SeenLog *)data;
|
|
if ( log->count < CAP ) {
|
|
snprintf(log->keys[log->count], AKSL_HASHMAP_MAX_KEY, "%s", key);
|
|
}
|
|
log->count += 1;
|
|
if ( log->break_at == log->count - 1 ) {
|
|
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
|
|
}
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* FNV-1a */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* The reference values are the canonical 32-bit FNV-1a ones: h = 2166136261,
|
|
* then h = (h XOR byte) * 16777619 for each byte. "a" and "foobar" are the
|
|
* vectors from the FNV reference material.
|
|
*/
|
|
static int test_fnv1a_known_answers(void)
|
|
{
|
|
uint32_t h = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_fnv1a("", 0, &h));
|
|
AKSL_CHECK(h == 2166136261u);
|
|
AKSL_CHECK_OK(aksl_strhash_fnv1a("a", 1, &h));
|
|
AKSL_CHECK(h == 0xe40c292cu);
|
|
AKSL_CHECK_OK(aksl_strhash_fnv1a("foobar", 6, &h));
|
|
AKSL_CHECK(h == 0xbf9cf968u);
|
|
|
|
/* The NUL-terminated form agrees with the length-driven one. */
|
|
AKSL_CHECK_OK(aksl_strhash_fnv1a_str("foobar", &h));
|
|
AKSL_CHECK(h == 0xbf9cf968u);
|
|
|
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, &h), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a("a", 1, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str(NULL, &h), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str("a", NULL), AKERR_NULLPOINTER);
|
|
return 0;
|
|
}
|
|
|
|
/* High bytes are unsigned here for the same reason they are in djb2. */
|
|
static int test_fnv1a_high_bit_bytes_are_unsigned(void)
|
|
{
|
|
char buf[2] = { (char)0xff, (char)0xfe };
|
|
uint32_t h = 0;
|
|
uint32_t expected = 2166136261u;
|
|
|
|
expected = (expected ^ 0xffu) * 16777619u;
|
|
expected = (expected ^ 0xfeu) * 16777619u;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_fnv1a(buf, sizeof(buf), &h));
|
|
AKSL_CHECK(h == expected);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Put and get */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_put_and_get(void)
|
|
{
|
|
aksl_HashEntry slots[CAP];
|
|
aksl_HashMap map;
|
|
int a = 1;
|
|
int b = 2;
|
|
void *value = NULL;
|
|
int found = 99;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
|
AKSL_CHECK(map.count == 0);
|
|
AKSL_CHECK(map.capacity == CAP);
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a));
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "beta", &b));
|
|
AKSL_CHECK(map.count == 2);
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", &value, &found));
|
|
AKSL_CHECK(found == 1);
|
|
AKSL_CHECK(value == &a);
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "beta", &value, &found));
|
|
AKSL_CHECK(found == 1);
|
|
AKSL_CHECK(value == &b);
|
|
|
|
/* A key that is not there is found = 0 and success, not an error: looking
|
|
* something up and not finding it is the ordinary case for a symbol table. */
|
|
value = (void *)0x1;
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "gamma", &value, &found));
|
|
AKSL_CHECK(found == 0);
|
|
AKSL_CHECK(value == (void *)0x1); /* untouched when the key is absent */
|
|
|
|
/* NULL value out-param is allowed: sometimes you only want to know if it is there. */
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found));
|
|
AKSL_CHECK(found == 1);
|
|
return 0;
|
|
}
|
|
|
|
/* Putting an existing key replaces the value rather than adding a second entry. */
|
|
static int test_put_replaces_an_existing_key(void)
|
|
{
|
|
aksl_HashEntry slots[CAP];
|
|
aksl_HashMap map;
|
|
int a = 1;
|
|
int b = 2;
|
|
void *value = NULL;
|
|
int found = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &a));
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &b));
|
|
AKSL_CHECK(map.count == 1);
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "key", &value, &found));
|
|
AKSL_CHECK(found == 1);
|
|
AKSL_CHECK(value == &b);
|
|
return 0;
|
|
}
|
|
|
|
/* The map copies keys into its slots, so it owns them and cannot outlive them. */
|
|
static int test_the_map_owns_its_keys(void)
|
|
{
|
|
aksl_HashEntry slots[CAP];
|
|
aksl_HashMap map;
|
|
char key[16] = "transient";
|
|
int a = 1;
|
|
int found = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, &a));
|
|
|
|
/* Scribble over the caller's copy of the key. */
|
|
memset(key, 'Z', sizeof(key) - 1);
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "transient", NULL, &found));
|
|
AKSL_CHECK(found == 1);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Bounds */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* Full is an error naming the capacity, not a silent resize. A table that
|
|
* reallocates is a table whose entry pointers move underneath anything holding
|
|
* one; this one has a worst case you can state.
|
|
*/
|
|
static int test_a_full_map_refuses_rather_than_resizing(void)
|
|
{
|
|
aksl_HashEntry slots[4];
|
|
aksl_HashMap map;
|
|
char key[16];
|
|
int i = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
|
for ( i = 0; i < 4; i++ ) {
|
|
snprintf(key, sizeof(key), "k%d", i);
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
|
|
}
|
|
AKSL_CHECK(map.count == 4);
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, "overflow", NULL),
|
|
AKERR_OUTOFBOUNDS, "does not resize");
|
|
AKSL_CHECK(map.count == 4);
|
|
|
|
/* Everything already in there is still reachable and still correct. */
|
|
for ( i = 0; i < 4; i++ ) {
|
|
int found = 0;
|
|
snprintf(key, sizeof(key), "k%d", i);
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found));
|
|
AKSL_CHECK(found == 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* A key too long for a slot is refused rather than truncated. Truncation would
|
|
* silently collide two different keys that share a prefix, which is the worst
|
|
* possible failure for a symbol table -- a lookup that returns the wrong thing.
|
|
*/
|
|
static int test_an_oversized_key_is_refused(void)
|
|
{
|
|
aksl_HashEntry slots[CAP];
|
|
aksl_HashMap map;
|
|
char longkey[AKSL_HASHMAP_MAX_KEY + 8];
|
|
int found = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
|
memset(longkey, 'k', sizeof(longkey) - 1);
|
|
longkey[sizeof(longkey) - 1] = '\0';
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, longkey, NULL),
|
|
AKERR_OUTOFBOUNDS, "AKSL_HASHMAP_MAX_KEY");
|
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, longkey, NULL, &found), AKERR_OUTOFBOUNDS);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
|
|
|
|
/* One byte short of the limit is fine: it is a limit, not an off-by-one. */
|
|
longkey[AKSL_HASHMAP_MAX_KEY - 1] = '\0';
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, longkey, NULL));
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, longkey, NULL, &found));
|
|
AKSL_CHECK(found == 1);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Removal and tombstones */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_remove(void)
|
|
{
|
|
aksl_HashEntry slots[CAP];
|
|
aksl_HashMap map;
|
|
int a = 1;
|
|
int found = 99;
|
|
int removed = 99;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a));
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed));
|
|
AKSL_CHECK(removed == 1);
|
|
AKSL_CHECK(map.count == 0);
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found));
|
|
AKSL_CHECK(found == 0);
|
|
|
|
/* Removing what is not there is success with removed = 0. */
|
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed));
|
|
AKSL_CHECK(removed == 0);
|
|
/* The out-param is optional. */
|
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "nothing", NULL));
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* The tombstone case, which is the one an open-addressed table gets wrong.
|
|
*
|
|
* A tiny capacity forces collisions, so several keys share a probe chain.
|
|
* Deleting one from the middle of that chain must not cut the keys behind it
|
|
* loose: if the slot were simply emptied, the probe for a later key would stop
|
|
* there and report the key missing while it was still sitting in the table.
|
|
*/
|
|
static int test_removal_does_not_break_a_probe_chain(void)
|
|
{
|
|
aksl_HashEntry slots[4];
|
|
aksl_HashMap map;
|
|
char key[16];
|
|
int found = 0;
|
|
int i = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
|
for ( i = 0; i < 4; i++ ) {
|
|
snprintf(key, sizeof(key), "k%d", i);
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
|
|
}
|
|
|
|
/* Take out each key in turn and confirm every remaining one is still found. */
|
|
for ( i = 0; i < 4; i++ ) {
|
|
int j = 0;
|
|
snprintf(key, sizeof(key), "k%d", i);
|
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL));
|
|
for ( j = i + 1; j < 4; j++ ) {
|
|
snprintf(key, sizeof(key), "k%d", j);
|
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found));
|
|
AKSL_CHECK(found == 1);
|
|
}
|
|
}
|
|
AKSL_CHECK(map.count == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* A tombstone is reusable, so a table churned through repeatedly does not fill up. */
|
|
static int test_tombstones_are_reused(void)
|
|
{
|
|
aksl_HashEntry slots[4];
|
|
aksl_HashMap map;
|
|
char key[16];
|
|
int i = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
|
/* Far more insert/remove cycles than there are slots. */
|
|
for ( i = 0; i < 64; i++ ) {
|
|
snprintf(key, sizeof(key), "cycle%d", i);
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
|
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL));
|
|
}
|
|
AKSL_CHECK(map.count == 0);
|
|
/* Still usable afterwards. */
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "final", NULL));
|
|
AKSL_CHECK(map.count == 1);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Iteration */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_iterate_visits_every_live_entry(void)
|
|
{
|
|
aksl_HashEntry slots[CAP];
|
|
aksl_HashMap map;
|
|
SeenLog log;
|
|
char key[16];
|
|
int i = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
|
for ( i = 0; i < 5; i++ ) {
|
|
snprintf(key, sizeof(key), "k%d", i);
|
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
|
|
}
|
|
/* One removed, so there is a tombstone for the walk to skip. */
|
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "k2", NULL));
|
|
|
|
memset((void *)&log, 0x00, sizeof(log));
|
|
log.break_at = -1;
|
|
AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log));
|
|
AKSL_CHECK(log.count == 4);
|
|
|
|
/* The break stops it, as everywhere else in this library. */
|
|
memset((void *)&log, 0x00, sizeof(log));
|
|
log.break_at = 1;
|
|
AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log));
|
|
AKSL_CHECK(log.count == 2);
|
|
return 0;
|
|
}
|
|
|
|
static int test_rejects_null_and_uninitialised(void)
|
|
{
|
|
aksl_HashEntry slots[CAP];
|
|
aksl_HashMap map;
|
|
aksl_HashMap empty;
|
|
SeenLog log;
|
|
int found = 0;
|
|
|
|
memset((void *)&empty, 0x00, sizeof(empty));
|
|
memset((void *)&log, 0x00, sizeof(log));
|
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
|
|
|
AKSL_CHECK_STATUS(aksl_hashmap_init(NULL, slots, CAP), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_init(&map, NULL, CAP), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_init(&map, slots, 0), AKERR_VALUE);
|
|
|
|
AKSL_CHECK_STATUS(aksl_hashmap_put(NULL, "k", NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, NULL, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_get(NULL, "k", NULL, &found), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, NULL, NULL, &found), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "k", NULL, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_remove(NULL, "k", NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, NULL, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_iterate(NULL, &record_entry, &log), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_iterate(&map, NULL, &log), AKERR_NULLPOINTER);
|
|
|
|
/* A map that was never initialised is caught rather than dereferenced. */
|
|
AKSL_CHECK_STATUS(aksl_hashmap_put(&empty, "k", NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&empty, "k", NULL, &found), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_remove(&empty, "k", NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_hashmap_iterate(&empty, &record_entry, &log), AKERR_NULLPOINTER);
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int failures = 0;
|
|
|
|
akerr_init();
|
|
|
|
AKSL_RUN(failures, test_fnv1a_known_answers);
|
|
AKSL_RUN(failures, test_fnv1a_high_bit_bytes_are_unsigned);
|
|
|
|
AKSL_RUN(failures, test_put_and_get);
|
|
AKSL_RUN(failures, test_put_replaces_an_existing_key);
|
|
AKSL_RUN(failures, test_the_map_owns_its_keys);
|
|
|
|
AKSL_RUN(failures, test_a_full_map_refuses_rather_than_resizing);
|
|
AKSL_RUN(failures, test_an_oversized_key_is_refused);
|
|
|
|
AKSL_RUN(failures, test_remove);
|
|
AKSL_RUN(failures, test_removal_does_not_break_a_probe_chain);
|
|
AKSL_RUN(failures, test_tombstones_are_reused);
|
|
|
|
AKSL_RUN(failures, test_iterate_visits_every_live_entry);
|
|
AKSL_RUN(failures, test_rejects_null_and_uninitialised);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|