All checks were successful
Eighty-five comments cited section numbers -- 1.1, 2.2.6, 3.6 -- from a numbering the file had already abandoned before the move to the tracker. They label completed work, so the pointer was the only wrong part. The citation is removed and the sentence kept, which is what issue #27 recommended: these are labels, not references, and a label carrying a version-dependent pointer goes stale again at the next reorganisation. Where a pointer earns its place it names what actually holds the content now -- UPGRADING.md for the confirmed defects, libakerror #15 for the target namespacing, issue #7 for the mutation survivors. README.md and akstdlib.h sent readers to TODO.md for 'what is still open'; they name the tracker. Verified: cmake --build build && ctest --test-dir build, 19/19. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
418 lines
14 KiB
C
418 lines
14 KiB
C
/*
|
|
* The fixed-capacity hash map and FNV-1a -- src/collections.c.
|
|
*
|
|
* "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);
|
|
}
|