mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-20 05:40:17 +08:00
A slice read only by lending consumers (string equality and ordering, length/charCodeAt/indexOf/includes/startsWith/endsWith, Number/unary plus/parseInt/parseFloat, typed string-keyed Map/Set get/has/set/add/ delete keys, string switch discriminants, and string parameters their callee only lends) is written by scr_str_slice_scratch into a per-site stack buffer as an immortal ASCII ScrStr marked as scratch (cap < len), instead of a heap allocation. Stable locals initialized by such a slice qualify when every read lends. Map/Set insertion stores a heap copy of a scratch key, and probes cache the key hash in the scratch header, so the counting idiom allocates only for new keys. Longer, non-ASCII, and interned results keep the owned slice; coroutines and debug builds are unchanged. Slice bounds are normalized without a libm trunc call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
851 lines
36 KiB
C
851 lines
36 KiB
C
/* Unit tests for the map runtime (scr_map.c). Built with ASan +
|
|
* -DSCR_RC_AUDIT by map.test.ts. Prints "N/N cases passed" to stderr.
|
|
*
|
|
* Focus areas the differential corpus cannot reach directly:
|
|
* - SameValueZero exactness at the C level (canonical-NaN hashing, the
|
|
* stored -0 key normalizing to +0);
|
|
* - RC accounting: string keys/values counted live, overwrite releasing the
|
|
* old value, delete releasing key+value, clear releasing everything;
|
|
* - tombstone/compaction behavior under churn (entries stay bounded);
|
|
* - live-iteration index stability while iter_depth is held.
|
|
*/
|
|
#include "../src/scr_runtime.h"
|
|
#include "../src/scr_key.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
long scr_str_live_count(void); /* provided by scr_string.c */
|
|
long scr_map_live_count(void); /* provided by scr_map.c */
|
|
long scr_arr_live_count(void);
|
|
long scr_union_live_count(void);
|
|
#endif
|
|
|
|
static long total = 0, failed = 0;
|
|
|
|
static void check(bool ok, const char *what) {
|
|
total++;
|
|
if (!ok) {
|
|
failed++;
|
|
fprintf(stderr, "FAIL: %s\n", what);
|
|
}
|
|
}
|
|
|
|
static ScrStr *S(const char *s) { return scr_str_new(s, strlen(s)); }
|
|
|
|
/* Map descriptors for the tests, kept in a pool that outlives every map. */
|
|
static const ScrMapOps *ops_of(void *(*key_retain)(void *), void (*key_release)(void *),
|
|
ScrTraceFn key_trace, void *(*val_retain)(void *),
|
|
void (*val_release)(void *), ScrTraceFn val_trace,
|
|
const uint8_t *union_keys) {
|
|
static ScrMapOps pool[32];
|
|
static size_t used = 0;
|
|
ScrMapOps want = { key_retain, key_release, key_trace, val_retain, val_release, val_trace, union_keys };
|
|
for (size_t i = 0; i < used; i++) {
|
|
if (memcmp(&pool[i], &want, sizeof want) == 0) return &pool[i];
|
|
}
|
|
if (used == sizeof pool / sizeof pool[0]) abort();
|
|
pool[used] = want;
|
|
return &pool[used++];
|
|
}
|
|
|
|
/* Exact-sized allocations leave no readable terminator or tail padding.
|
|
* Exercise the load boundaries at each alignment, including embedded zero
|
|
* bytes and a mismatch at every position. */
|
|
static void test_key_bytes(void) {
|
|
for (size_t length = 0; length <= 65; length++) {
|
|
for (size_t offset = 0; offset < 8; offset++) {
|
|
char *allocation = malloc(offset + length + 1);
|
|
char *a = allocation + offset + 1;
|
|
char *b = malloc(length ? length : 1);
|
|
for (size_t i = 0; i < length; i++) a[i] = b[i] = (char)(i * 71);
|
|
uint64_t hash = scr_key_hash(a, length);
|
|
check(hash == scr_key_hash(b, length) && scr_key_equal(a, b, length),
|
|
"unaligned content-equal byte keys agree");
|
|
for (size_t i = 0; i < length; i++) {
|
|
b[i] ^= 1;
|
|
check(!scr_key_equal(a, b, length), "every key byte participates in equality");
|
|
b[i] ^= 1;
|
|
}
|
|
free(b);
|
|
free(allocation);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void test_string_bucket_collisions(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
|
ScrStr *keys[12];
|
|
size_t count = 0;
|
|
for (unsigned i = 0; i < 65536 && count < 12; i++) {
|
|
char bytes[9];
|
|
snprintf(bytes, sizeof bytes, "%08x", i);
|
|
uint64_t hash = scr_key_hash(bytes, 8);
|
|
if (((uint32_t)(hash ^ (hash >> 32)) & 127) != 0) continue; /* folded map hash */
|
|
keys[count] = scr_str_new(bytes, 8);
|
|
scr_map_set_str_f64(m, keys[count], (double)count);
|
|
count++;
|
|
}
|
|
check(count == 12 && m->nbuckets == 32, "colliding keys promote to bucket storage");
|
|
scr_map_iter_enter(m);
|
|
for (size_t i = 0; i < count; i += 2) {
|
|
check(scr_map_delete_str(m, keys[i]), "collision chain deletion");
|
|
}
|
|
for (size_t i = 0; i < count; i++) {
|
|
ScrStr *probe = scr_str_new(keys[i]->data, keys[i]->len);
|
|
double out = -1;
|
|
bool found = scr_map_get_str_f64(m, probe, &out);
|
|
check(found == (i % 2 != 0) && (!found || out == (double)i),
|
|
"content probe crosses collision-chain tombstones");
|
|
out = -1;
|
|
found = scr_map_get_span_f64(m, probe->data, probe->len, &out);
|
|
check(found == (i % 2 != 0) && (!found || out == (double)i),
|
|
"span probe crosses collision-chain tombstones");
|
|
scr_map_set_str_f64(m, probe, (double)i + 100);
|
|
check(scr_map_has_str(m, probe), "collision-chain overwrite or reinsertion");
|
|
scr_str_release(probe);
|
|
scr_str_release(keys[i]);
|
|
}
|
|
scr_map_iter_exit(m);
|
|
scr_map_clear(m);
|
|
ScrStr *empty = S("");
|
|
check(!scr_map_has_str(m, empty), "cleared bucket table misses without hashing");
|
|
scr_str_release(empty);
|
|
scr_map_release(m);
|
|
}
|
|
|
|
static void test_string_keys(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
|
ScrStr *ka = S("alpha");
|
|
ScrStr *kb = S("beta");
|
|
scr_map_set_str_f64(m, ka, 1);
|
|
scr_map_set_str_f64(m, kb, 2);
|
|
check(scr_map_size(m) == 2, "size after two sets");
|
|
scr_map_set_str_f64(m, ka, 10);
|
|
check(scr_map_size(m) == 2, "overwrite keeps size");
|
|
double out = 0;
|
|
check(scr_map_get_str_f64(m, ka, &out) && out == 10, "overwritten value reads back");
|
|
/* content equality: a DIFFERENT ScrStr with the same bytes hits */
|
|
ScrStr *ka2 = S("alpha");
|
|
check(scr_map_has_str(m, ka2), "content-equal key found");
|
|
check(scr_map_delete_str(m, ka2), "delete by content-equal key");
|
|
check(!scr_map_delete_str(m, ka2), "second delete misses");
|
|
check(!scr_map_has_str(m, ka), "deleted key gone");
|
|
scr_str_release(ka2);
|
|
scr_map_clear(m);
|
|
check(scr_map_size(m) == 0 && !scr_map_has_str(m, kb), "clear empties");
|
|
scr_str_release(ka);
|
|
scr_str_release(kb);
|
|
scr_map_release(m);
|
|
}
|
|
|
|
/* scr_map_entry_str: the entry of a string key, for generated code that
|
|
* reads the value slot and retains it with the value type's own retain. The
|
|
* lookup itself takes no reference, in linear and bucketed maps alike. */
|
|
static void test_string_entries(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_REF, ops_of(NULL, NULL, NULL, scr_str_retain_v, scr_str_release_v, NULL, NULL));
|
|
ScrStr *missing = S("missing");
|
|
check(scr_map_entry_str(m, missing) == NULL, "empty map has no entry");
|
|
char name[16];
|
|
for (int i = 0; i < 40; i++) {
|
|
snprintf(name, sizeof name, "key%d", i);
|
|
ScrStr *k = S(name);
|
|
scr_map_set_str_ref(m, k, S(name)); /* value +1 moves in */
|
|
scr_str_release(k);
|
|
ScrStr *probe = S("key0"); /* a different object with equal bytes */
|
|
const ScrMapEntry *e = scr_map_entry_str(m, probe);
|
|
check(e != NULL, "entry found by content");
|
|
ScrStr *v = (ScrStr *)(uintptr_t)e->val;
|
|
check(v->len == 4 && memcmp(v->data, "key0", 4) == 0, "entry value slot holds the value");
|
|
check(v->rc == 1, "entry lookup takes no reference");
|
|
scr_str_release(probe);
|
|
}
|
|
check(scr_map_entry_str(m, missing) == NULL, "absent key has no entry");
|
|
ScrStr *k7 = S("key7");
|
|
check(scr_map_delete_str(m, k7), "delete");
|
|
check(scr_map_entry_str(m, k7) == NULL, "deleted key has no entry");
|
|
scr_str_release(k7);
|
|
scr_str_release(missing);
|
|
scr_map_release(m);
|
|
}
|
|
|
|
/* String keys hash to 32 bits; heap strings cache the value in their header
|
|
* and static strings (compiler literals, runtime one-character strings)
|
|
* carry it precomputed. The vectors are shared with the compiler's
|
|
* string-key-hash.test.ts: literal keys only work if both sides agree. */
|
|
static const struct { const char *bytes; size_t length; uint32_t hash; } hash_vectors[] = {
|
|
{"", 0, 0x993d6596u},
|
|
{"a", 1, 0x16793313u},
|
|
{"q", 1, 0x81558571u},
|
|
{"ab", 2, 0x2985501fu},
|
|
{"abc", 3, 0xd7f8c3adu},
|
|
{"alpha", 5, 0x837effcdu},
|
|
{"kalomi", 6, 0x4ed53d7du},
|
|
{"hello world!", 12, 0xd1ecb7bbu},
|
|
{"\xEF\xBF\xBD", 3, 0xeba2c495u},
|
|
{"a much longer key of 31 bytes!!", 31, 0x1a2fe80fu},
|
|
{"\xC3\xA9t\xC3\xA9", 5, 0x17c315c4u},
|
|
};
|
|
|
|
static void test_string_hash_cache(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
|
for (size_t i = 0; i < sizeof hash_vectors / sizeof hash_vectors[0]; i++) {
|
|
ScrStr *key = scr_str_new(hash_vectors[i].bytes, hash_vectors[i].length);
|
|
scr_map_set_str_f64(m, key, (double)i);
|
|
#if SCR_STR_HASH_CACHE
|
|
check(key->hash == hash_vectors[i].hash, "pinned 32-bit key hash (shared with the compiler)");
|
|
check(key->cap == key->len, "hash half leaves the capacity intact");
|
|
#endif
|
|
double out = -1;
|
|
check(scr_map_get_span_f64(m, hash_vectors[i].bytes, hash_vectors[i].length, &out) && out == (double)i,
|
|
"span probes hash like cached strings");
|
|
scr_str_release(key);
|
|
}
|
|
scr_map_release(m);
|
|
|
|
/* Immortal one-character and empty strings carry the same hash as heap
|
|
* strings with equal bytes, so either form finds the other's entry. */
|
|
m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
|
ScrStr *source = S("qa");
|
|
ScrStr *q = scr_str_char_at(source, 0);
|
|
ScrStr *empty = scr_str_char_at(source, 9);
|
|
check(q->rc == SIZE_MAX && empty->rc == SIZE_MAX, "one-character and empty strings are immortal");
|
|
#if SCR_STR_HASH_CACHE
|
|
check(q->hash == 0x81558571u && q->cap == 1, "static one-character hash precomputed");
|
|
check(empty->hash == 0x993d6596u && empty->cap == 0, "static empty-string hash precomputed");
|
|
#endif
|
|
ScrStr *heap_q = S("q");
|
|
ScrStr *heap_empty = S("");
|
|
scr_map_set_str_f64(m, q, 1);
|
|
scr_map_set_str_f64(m, heap_empty, 2);
|
|
double out = 0;
|
|
check(scr_map_get_str_f64(m, heap_q, &out) && out == 1, "heap probe finds static key");
|
|
check(scr_map_get_str_f64(m, empty, &out) && out == 2, "static probe finds heap key");
|
|
scr_str_release(heap_q);
|
|
scr_str_release(heap_empty);
|
|
scr_str_release(source);
|
|
scr_map_clear(m);
|
|
|
|
/* A uniquely owned string appended in place must drop its cached hash:
|
|
* a stale hash would probe the old content's bucket and miss. */
|
|
ScrStr *ab = S("ab");
|
|
ScrStr *cd = S("cd");
|
|
ScrStr *grown = scr_str_concat(ab, cd); /* rc 1, spare capacity */
|
|
scr_str_release(ab);
|
|
ScrStr *full = S("abcde");
|
|
scr_map_set_str_f64(m, full, 5);
|
|
check(!scr_map_has_str(m, grown), "prefix misses (and caches its hash)");
|
|
ScrStr *e = S("e");
|
|
ScrStr *appended = scr_str_concat(grown, e);
|
|
check(appended == grown, "unique string appended in place");
|
|
check(scr_map_get_str_f64(m, appended, &out) && out == 5, "in-place append invalidates the cached hash");
|
|
scr_str_release(appended);
|
|
scr_str_release(grown);
|
|
scr_str_release(e);
|
|
scr_str_release(cd);
|
|
scr_str_release(full);
|
|
scr_map_release(m);
|
|
}
|
|
|
|
/* Probes need neither a terminator nor readable padding and never retain
|
|
* their source. Returned references keep independent snapshot ownership. */
|
|
static void test_span_keys(void) {
|
|
ScrMap *numbers = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
|
ScrMap *flags = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_BOOL, NULL);
|
|
ScrMap *refs = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_REF,
|
|
ops_of(NULL, NULL, NULL, scr_str_retain_v, scr_str_release_v, NULL, NULL));
|
|
for (size_t length = 0; length < 70; length++) {
|
|
char *bytes = malloc(length ? length : 1);
|
|
for (size_t i = 0; i < length; i++) bytes[i] = (char)(i * 71);
|
|
ScrStr *key = scr_str_new(bytes, length);
|
|
scr_map_set_str_f64(numbers, key, (double)length);
|
|
scr_map_set_str_bool(flags, key, length % 2 != 0);
|
|
scr_map_set_str_ref(refs, key, scr_str_retain(key));
|
|
double number = -1;
|
|
bool flag = false;
|
|
check(scr_map_get_span_f64(numbers, bytes, length, &number) && number == (double)length,
|
|
"bounded span finds number key");
|
|
check(scr_map_get_span_bool(flags, bytes, length, &flag) && flag == (length % 2 != 0),
|
|
"bounded span finds boolean key");
|
|
ScrStr *snapshot = scr_map_get_span_ref(refs, bytes, length);
|
|
scr_map_clear(refs);
|
|
check(snapshot && scr_str_eq(snapshot, key), "span result survives clearing its map");
|
|
scr_str_release(snapshot);
|
|
check(scr_map_delete_str(numbers, key) && !scr_map_get_span_f64(numbers, bytes, length, &number),
|
|
"span misses deleted entry");
|
|
scr_str_release(key);
|
|
free(bytes);
|
|
}
|
|
scr_map_release(refs);
|
|
scr_map_release(flags);
|
|
scr_map_release(numbers);
|
|
}
|
|
|
|
/* A scratch slice probes like any key; an insertion stores a heap copy, so
|
|
* the stored key survives the buffer's next contents. */
|
|
static void test_scratch_keys(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
|
ScrStr *line = S("alpha beta alpha");
|
|
_Alignas(16) unsigned char buffer[96];
|
|
ScrStr *alpha = scr_str_slice_scratch(line, 0, 5, false, (ScrStr *)buffer, sizeof buffer);
|
|
check(alpha == (ScrStr *)buffer && scr_str_is_scratch(alpha), "short ASCII slice uses the buffer");
|
|
scr_map_set_str_f64(m, alpha, 1);
|
|
double out = 0;
|
|
check(scr_map_get_str_f64(m, alpha, &out) && out == 1, "scratch key probes its entry");
|
|
ScrStr *beta = scr_str_slice_scratch(line, 6, 10, false, (ScrStr *)buffer, sizeof buffer);
|
|
check(!scr_map_has_str(m, beta), "refilled buffer probes its new text");
|
|
scr_map_set_str_f64(m, beta, 2);
|
|
ScrStr *again = scr_str_slice_scratch(line, 11, 16, false, (ScrStr *)buffer, sizeof buffer);
|
|
scr_map_set_str_f64(m, again, 3);
|
|
check(scr_map_size(m) == 2, "scratch overwrite keeps one entry");
|
|
ScrStr *first = scr_map_iter_key_str(m, 0), *second = scr_map_iter_key_str(m, 1);
|
|
ScrStr *a = S("alpha"), *b = S("beta");
|
|
check(!scr_str_is_scratch(first) && scr_str_eq(first, a), "stored key is an owned copy");
|
|
check(!scr_str_is_scratch(second) && scr_str_eq(second, b), "second stored key is a copy");
|
|
check(scr_map_get_str_f64(m, a, &out) && out == 3, "overwrite through scratch key");
|
|
scr_str_release(a);
|
|
scr_str_release(b);
|
|
scr_str_release(first);
|
|
scr_str_release(second);
|
|
scr_str_release(line);
|
|
scr_map_release(m);
|
|
}
|
|
|
|
static void test_same_value_zero(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_BOOL, NULL);
|
|
scr_map_set_f64_bool(m, 0.0 / 0.0, true); /* NaN key */
|
|
check(scr_map_has_f64(m, NAN), "NaN finds NaN");
|
|
check(scr_map_has_f64(m, -NAN), "any NaN bit pattern finds NaN");
|
|
check(scr_map_size(m) == 1, "one NaN entry");
|
|
scr_map_set_f64_bool(m, -0.0, true);
|
|
check(scr_map_has_f64(m, 0.0), "-0 and +0 are one key");
|
|
/* the STORED key normalized to +0 (JS: [...m.keys()] shows 0) */
|
|
double k1 = scr_map_iter_key_f64(m, 1);
|
|
check(k1 == 0.0 && !signbit(k1), "stored -0 key normalized to +0");
|
|
scr_map_set_f64_bool(m, 0.0, false);
|
|
check(scr_map_size(m) == 2, "set(+0) overwrote the -0 entry");
|
|
bool b = true;
|
|
check(scr_map_get_f64_bool(m, -0.0, &b) && !b, "get(-0) reads the +0 entry");
|
|
check(scr_map_delete_f64(m, 0.0 / 0.0), "delete by NaN");
|
|
check(scr_map_size(m) == 1, "NaN entry deleted");
|
|
scr_map_release(m);
|
|
}
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
static void test_rc_accounting(void) {
|
|
long strings0 = scr_str_live_count();
|
|
long maps0 = scr_map_live_count();
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_REF,
|
|
ops_of(NULL, NULL, NULL, scr_str_retain_v, scr_str_release_v, NULL, NULL));
|
|
check(scr_map_live_count() == maps0 + 1, "map counted live");
|
|
ScrStr *k = S("key");
|
|
scr_map_set_str_ref(m, k, S("v1")); /* value +1 moves in; key retained */
|
|
scr_str_release(k); /* map keeps its own key reference */
|
|
check(scr_str_live_count() == strings0 + 2, "key + value live");
|
|
k = S("key");
|
|
ScrStr *v2 = S("v2");
|
|
scr_map_set_str_ref(m, k, scr_str_retain(v2)); /* replaces AND releases v1 */
|
|
check(scr_str_live_count() == strings0 + 3, "overwrite released the old value");
|
|
ScrStr *got = scr_map_get_str_ref(m, k);
|
|
check(got != NULL && scr_str_eq(got, v2), "get returns the new value");
|
|
scr_str_release(got);
|
|
scr_str_release(v2);
|
|
check(scr_map_delete_str(m, k), "delete");
|
|
scr_str_release(k);
|
|
check(scr_str_live_count() == strings0, "delete released key and value");
|
|
k = S("a");
|
|
scr_map_set_str_ref(m, k, S("1"));
|
|
scr_str_release(k);
|
|
k = S("b");
|
|
scr_map_set_str_ref(m, k, S("2"));
|
|
scr_str_release(k);
|
|
scr_map_release(m); /* releasing the map releases every entry */
|
|
check(scr_str_live_count() == strings0, "map release freed all entries");
|
|
check(scr_map_live_count() == maps0, "map freed");
|
|
}
|
|
#endif
|
|
|
|
static void test_churn_stays_bounded(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL);
|
|
for (int i = 0; i < 100000; i++) {
|
|
double k = (double)(i % 13);
|
|
scr_map_set_f64_f64(m, k, i);
|
|
if (i % 2) scr_map_delete_f64(m, k);
|
|
}
|
|
check(scr_map_size(m) <= 13, "churn size bounded");
|
|
/* compaction on growth keeps the dense array proportional to the live
|
|
* set, not to the 100k total insertions */
|
|
check(m->nentries <= 64, "tombstones compacted under churn");
|
|
scr_map_release(m);
|
|
}
|
|
|
|
static void test_live_iteration_indices(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL);
|
|
for (int i = 0; i < 4; i++) scr_map_set_f64_f64(m, i, i * 10);
|
|
scr_map_iter_enter(m);
|
|
int visited = 0;
|
|
double sum = 0;
|
|
for (double i = 0; i < scr_map_iter_count(m); i += 1) {
|
|
if (!scr_map_iter_live(m, i)) continue;
|
|
visited++;
|
|
sum += scr_map_iter_val_f64(m, i);
|
|
if (i == 0) {
|
|
scr_map_delete_f64(m, 2); /* pending: skipped */
|
|
scr_map_set_f64_f64(m, 99, 990); /* appended: visited */
|
|
/* force growth while iterating: indices must stay stable */
|
|
for (int g = 0; g < 50; g++) scr_map_set_f64_f64(m, 1000 + g, 0);
|
|
}
|
|
}
|
|
scr_map_iter_exit(m);
|
|
check(visited == 4 + 50, "adds visited, delete skipped, indices stable");
|
|
check(sum == 0 + 10 + 30 + 990, "values read at stable indices");
|
|
/* clear during iteration keeps indices; adds after it are visited */
|
|
scr_map_iter_enter(m);
|
|
int seen = 0;
|
|
for (double i = 0; i < scr_map_iter_count(m); i += 1) {
|
|
if (!scr_map_iter_live(m, i)) continue;
|
|
seen++;
|
|
if (seen == 1) {
|
|
scr_map_clear(m);
|
|
scr_map_set_f64_f64(m, 7, 7);
|
|
}
|
|
}
|
|
scr_map_iter_exit(m);
|
|
check(seen == 2, "clear stops the walk; the post-clear add is visited");
|
|
check(scr_map_size(m) == 1, "one live entry after clear+add");
|
|
scr_map_release(m);
|
|
}
|
|
|
|
/* A small (linear) map that must grow its lookup index while an iteration
|
|
* is active may not compact: entry indices, tombstones and the append
|
|
* position stay put. Integer keys in a compact range move to the direct
|
|
* index (dense) from the fifth entry, any other number keys to the bucket
|
|
* table from the ninth; both transitions are covered by running the same
|
|
* script with `offset` 0 and 0.5. */
|
|
static void run_small_storage_transitions(double offset) {
|
|
bool dense = offset == 0;
|
|
int n = dense ? 4 : 8; /* entries a linear map holds before its index grows */
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL);
|
|
for (int i = 0; i < n; i++) scr_map_set_f64_f64(m, i + offset, i + 10);
|
|
check(m->nbuckets == 0 && m->buckets == NULL && m->ndense == 0,
|
|
"small map has no index allocation");
|
|
for (int i = 0; i < 100; i++) {
|
|
scr_map_delete_f64(m, n - 1 + offset);
|
|
scr_map_set_f64_f64(m, n - 1 + offset, i);
|
|
}
|
|
check(m->nbuckets == 0 && m->ndense == 0 && m->nentries == (uint32_t)n,
|
|
"small churn compacts without an index");
|
|
ScrMap *copy = scr_map_clone(m, false, NULL);
|
|
check(copy->nbuckets == 0 && copy->ndense == 0, "small clone preserves compact storage");
|
|
scr_map_iter_enter(m);
|
|
scr_map_delete_f64(m, 1 + offset);
|
|
scr_map_set_f64_f64(m, n + offset, 14);
|
|
check(dense ? m->ndense > 0 && m->nbuckets == 0 : m->nbuckets > 0 && m->ndense == 0,
|
|
"live iteration promotes without moving entry indices");
|
|
check(m->nentries == (uint32_t)n + 1 && !scr_map_iter_live(m, 1) &&
|
|
scr_map_iter_key_f64(m, n) == n + offset,
|
|
"promotion preserves tombstones and append position");
|
|
for (int i = n + 1; i < 64; i++) scr_map_set_f64_f64(m, i + offset, i + 10);
|
|
check(m->nentries == 64 && !scr_map_iter_live(m, 1) && scr_map_iter_key_f64(m, 63) == 63 + offset,
|
|
"growth during iteration keeps entry indices");
|
|
double out = 0;
|
|
check(scr_map_get_f64_f64(m, offset, &out) && out == 10,
|
|
"original key survives promotion and growth");
|
|
check(!scr_map_has_f64(m, 1 + offset), "deleted small key stays absent after rehash");
|
|
scr_map_iter_exit(m);
|
|
check(scr_map_has_f64(copy, 1 + offset) && !scr_map_has_f64(copy, n + offset),
|
|
"small clone remains independent");
|
|
scr_map_clear(copy);
|
|
scr_map_iter_enter(copy);
|
|
for (int i = 0; i < n + 2; i++) {
|
|
scr_map_clear(copy);
|
|
scr_map_set_f64_f64(copy, i + offset, i);
|
|
}
|
|
check((dense ? copy->ndense > 0 : copy->nbuckets > 0) && copy->nentries == (uint32_t)n + 2 &&
|
|
copy->nlive == 1,
|
|
"clear and append preserve active small iterator positions");
|
|
check(!scr_map_iter_live(copy, n) && scr_map_iter_live(copy, n + 1) &&
|
|
scr_map_iter_key_f64(copy, n + 1) == n + 1 + offset,
|
|
"post-clear append lands past the iterator's tombstones");
|
|
scr_map_iter_exit(copy);
|
|
check(scr_map_has_f64(copy, n + 1 + offset) && !scr_map_has_f64(copy, n + offset),
|
|
"post-clear entry survives iterator compaction");
|
|
scr_map_release(copy);
|
|
scr_map_release(m);
|
|
}
|
|
|
|
static void test_small_storage_transitions(void) {
|
|
run_small_storage_transitions(0);
|
|
run_small_storage_transitions(0.5);
|
|
}
|
|
|
|
static void test_identity_key_ownership(void) {
|
|
ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0);
|
|
ScrArr *b = scr_arr_new(SCR_ELEM_F64, 0);
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_REF, SCR_MAP_VAL_REF,
|
|
ops_of(scr_arr_retain_v, scr_arr_release_v, NULL,
|
|
scr_str_retain_v, scr_str_release_v, NULL, NULL));
|
|
scr_map_set_ref_ref(m, a, S("first"));
|
|
scr_map_set_ref_ref(m, b, S("second"));
|
|
check(a->rc == 2 && b->rc == 2, "identity keys retained on insertion");
|
|
check(scr_map_size(m) == 2, "structurally equal arrays remain distinct keys");
|
|
scr_arr_push_f64(a, 42);
|
|
ScrStr *value = scr_map_get_ref_ref(m, a);
|
|
check(value && value->len == 5, "mutation does not change reference hash");
|
|
scr_str_release(value);
|
|
scr_map_set_ref_ref(m, a, S("replacement"));
|
|
check(a->rc == 2, "overwrite does not retain the existing key twice");
|
|
void *key = scr_map_iter_key_ref(m, 0);
|
|
check(key == a && a->rc == 3, "iteration returns the original key with ownership");
|
|
scr_arr_release(key);
|
|
ScrArr *absent = scr_arr_new(SCR_ELEM_F64, 0);
|
|
check(scr_map_get_ref_ref(m, absent) == NULL, "missing reference value returns NULL");
|
|
check(!scr_map_delete_ref(m, absent), "missing identity delete has no effect");
|
|
check(scr_map_delete_ref(m, a) && a->rc == 1, "delete releases its key immediately");
|
|
scr_map_clear(m);
|
|
check(b->rc == 1 && scr_map_size(m) == 0, "clear releases remaining reference keys");
|
|
scr_arr_release(a);
|
|
scr_arr_release(b);
|
|
scr_arr_release(absent);
|
|
scr_map_release(m);
|
|
}
|
|
|
|
static void test_identity_scalar_values(void) {
|
|
ScrArr *key = scr_arr_new(SCR_ELEM_F64, 0);
|
|
ScrArr *other = scr_arr_new(SCR_ELEM_F64, 0);
|
|
ScrMap *numbers = scr_map_new(SCR_MAP_KEY_REF, SCR_MAP_VAL_F64,
|
|
ops_of(scr_arr_retain_v, scr_arr_release_v, NULL, NULL, NULL, NULL, NULL));
|
|
ScrMap *flags = scr_map_new(SCR_MAP_KEY_REF, SCR_MAP_VAL_BOOL,
|
|
ops_of(scr_arr_retain_v, scr_arr_release_v, NULL, NULL, NULL, NULL, NULL));
|
|
double number = 99;
|
|
bool flag = true;
|
|
check(!scr_map_get_ref_f64(numbers, key, &number) && number == 99, "numeric miss preserves out parameter");
|
|
check(!scr_map_get_ref_bool(flags, key, &flag) && flag, "boolean miss preserves out parameter");
|
|
scr_map_set_ref_f64(numbers, key, -0.0);
|
|
check(scr_map_get_ref_f64(numbers, key, &number) && signbit(number), "reference-key numeric values preserve negative zero");
|
|
scr_map_set_ref_bool(flags, key, false);
|
|
check(scr_map_get_ref_bool(flags, key, &flag) && !flag, "false is present, not a missing entry");
|
|
scr_map_set_ref_bool(flags, key, true);
|
|
check(scr_map_get_ref_bool(flags, key, &flag) && flag, "boolean overwrite");
|
|
check(!scr_map_has_ref(numbers, other), "equal-content reference misses");
|
|
scr_map_release(numbers);
|
|
scr_map_release(flags);
|
|
check(key->rc == 1, "all scalar-valued maps release their keys");
|
|
scr_arr_release(key);
|
|
scr_arr_release(other);
|
|
}
|
|
|
|
static ScrUnion *wrap_key(uint32_t tag, ScrArr *key, ScrTraceFn trace) {
|
|
return scr_union_new_ref(tag, scr_arr_retain(key), scr_arr_retain_v, scr_arr_release_v, trace);
|
|
}
|
|
|
|
static void test_reboxed_keys_and_rehash(void) {
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_UNION_REF, SCR_MAP_VAL_F64,
|
|
ops_of(scr_union_retain_v, scr_union_release_v, NULL, NULL, NULL, NULL, NULL));
|
|
ScrArr *keys[128];
|
|
for (int i = 0; i < 128; i++) {
|
|
keys[i] = scr_arr_new(SCR_ELEM_F64, 0);
|
|
ScrUnion *box = wrap_key((uint32_t)(i % 3), keys[i], NULL);
|
|
scr_map_set_ref_f64(m, box, i);
|
|
scr_union_release(box);
|
|
}
|
|
check(scr_map_size(m) == 128, "union-key growth keeps all entries");
|
|
for (int i = 0; i < 128; i++) {
|
|
/* Tags and wrapper allocations are representation details. */
|
|
ScrUnion *probe = wrap_key(999, keys[i], NULL);
|
|
double value = -1;
|
|
check(scr_map_get_ref_f64(m, probe, &value) && value == i, "rehash uses payload identity");
|
|
if (i % 2 == 0) check(scr_map_delete_ref(m, probe), "delete through a fresh union wrapper");
|
|
scr_union_release(probe);
|
|
}
|
|
for (int i = 0; i < 128; i += 2) {
|
|
ScrUnion *box = wrap_key(7, keys[i], NULL);
|
|
scr_map_set_ref_f64(m, box, -i);
|
|
scr_union_release(box);
|
|
}
|
|
ScrUnion *first = scr_map_iter_key_ref(m, 0);
|
|
check(scr_union_peek(first) == keys[1], "reinserted union keys follow surviving insertion order");
|
|
scr_union_release(first);
|
|
ScrUnion *probe = wrap_key(55, keys[1], NULL);
|
|
ScrUnion *original = scr_map_iter_key_ref(m, 0);
|
|
scr_map_set_ref_f64(m, probe, 700);
|
|
ScrUnion *stored = scr_map_iter_key_ref(m, 0);
|
|
check(stored == original && stored != probe, "overwrite keeps the first stored key representation");
|
|
scr_union_release(stored);
|
|
scr_union_release(original);
|
|
scr_union_release(probe);
|
|
scr_map_clear(m);
|
|
for (int i = 0; i < 128; i++) {
|
|
check(keys[i]->rc == 1, "clear releases each stored box and its payload");
|
|
scr_arr_release(keys[i]);
|
|
}
|
|
scr_map_release(m);
|
|
}
|
|
|
|
static void test_value_keys_and_copies(void) {
|
|
static const uint8_t kinds[] = { SCR_MAP_KEY_F64, SCR_MAP_KEY_BOOL, SCR_MAP_KEY_REF, SCR_MAP_KEY_REF };
|
|
ScrMap *m = scr_map_new(SCR_MAP_KEY_UNION_VALUE, SCR_MAP_VAL_F64,
|
|
ops_of(scr_union_retain_v, scr_union_release_v, NULL, NULL, NULL, NULL, kinds));
|
|
ScrUnion *negative_zero = scr_union_new_f64(0, -0.0);
|
|
ScrUnion *zero = scr_union_new_f64(0, 0.0);
|
|
ScrUnion *flag = scr_union_new_bool(1, false);
|
|
scr_map_set_ref_f64(m, negative_zero, 10);
|
|
scr_map_set_ref_f64(m, flag, 20);
|
|
check(m->entries[0].hash == 1 && m->entries[1].hash == 1, "zero hashes remap without conflating key kinds");
|
|
check(signbit(scr_union_get_f64(negative_zero)), "normalization leaves caller's union unchanged");
|
|
ScrUnion *stored = scr_map_iter_key_ref(m, 0);
|
|
check(!signbit(scr_union_get_f64(stored)), "stored numeric union has positive zero");
|
|
scr_union_release(stored);
|
|
double out = 0;
|
|
check(scr_map_get_ref_f64(m, zero, &out) && out == 10, "value-equal union probe finds zero");
|
|
check(scr_map_get_ref_f64(m, flag, &out) && out == 20, "same full hash still checks primitive kind");
|
|
ScrArr *key = scr_arr_new(SCR_ELEM_F64, 0);
|
|
ScrUnion *left = wrap_key(2, key, NULL), *right = wrap_key(3, key, NULL);
|
|
scr_map_set_ref_f64(m, left, 30);
|
|
check(scr_map_has_ref(m, right), "reference identity survives different union arm tags");
|
|
scr_map_delete_ref(m, flag);
|
|
ScrMap *copy = scr_map_clone(m, false, NULL);
|
|
ScrMap *keys = scr_map_clone(m, true, NULL);
|
|
check(copy->nentries == 2 && copy->nlive == 2 && copy->ecap == 2, "copy allocates only live entries");
|
|
check(keys->nentries == 2 && keys->val_kind == SCR_MAP_VAL_F64, "key copy keeps compact Set storage");
|
|
scr_map_clear(m);
|
|
check(scr_map_get_ref_f64(copy, right, &out) && out == 30, "copy owns the original reference key");
|
|
scr_map_set_ref_f64(copy, flag, 40);
|
|
check(!scr_map_has_ref(keys, flag), "copy insertion leaves its sibling independent");
|
|
scr_union_release(negative_zero); scr_union_release(zero); scr_union_release(flag);
|
|
scr_union_release(left); scr_union_release(right);
|
|
scr_map_release(m); scr_map_release(copy); scr_map_release(keys);
|
|
check(key->rc == 1, "all copied union key owners released");
|
|
scr_arr_release(key);
|
|
|
|
ScrStr *decimal = S("18446744073709551616"), *hex = S("0x10000000000000000");
|
|
ScrBigInt *a = scr_bigint_parse(decimal), *b = scr_bigint_parse(hex);
|
|
scr_str_release(decimal); scr_str_release(hex);
|
|
check(scr_bigint_eq(a, b) && scr_bigint_hash(a) == scr_bigint_hash(b), "equal parsed bigint values hash alike");
|
|
ScrMap *big = scr_map_new(SCR_MAP_KEY_BIGINT, SCR_MAP_VAL_F64,
|
|
ops_of(scr_bigint_retain_v, scr_bigint_release_v, NULL, NULL, NULL, NULL, NULL));
|
|
scr_map_set_ref_f64(big, a, 1);
|
|
scr_map_set_ref_f64(big, b, 2);
|
|
scr_bigint_release(a);
|
|
check(big->nlive == 1 && scr_map_get_ref_f64(big, b, &out) && out == 2, "bigint keys compare values across owners");
|
|
scr_bigint_release(b);
|
|
scr_map_release(big);
|
|
}
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
static const ScrArrOps map_arr_ops = { scr_map_retain_v, scr_map_release_v, scr_map_trace_v };
|
|
|
|
static void test_identity_cycles(void) {
|
|
scr_collect_cycles();
|
|
long maps = scr_map_live_count();
|
|
long arrays = scr_arr_live_count();
|
|
long strings = scr_str_live_count();
|
|
long unions = scr_union_live_count();
|
|
for (int mode = 0; mode < 5; mode++) {
|
|
for (int i = 0; i < 100; i++) {
|
|
ScrArr *key = scr_arr_new_ops(&map_arr_ops, 0);
|
|
bool wrapped = mode >= 3;
|
|
ScrMapValKind value_kind = mode == 0 ? SCR_MAP_VAL_F64 : SCR_MAP_VAL_REF;
|
|
ScrTraceFn value_trace = mode == 2 ? scr_arr_trace_v : NULL;
|
|
static const uint8_t union_kinds[] = { SCR_MAP_KEY_REF };
|
|
ScrMap *m = scr_map_new(mode == 4 ? SCR_MAP_KEY_UNION_VALUE : wrapped ? SCR_MAP_KEY_UNION_REF : SCR_MAP_KEY_REF, value_kind,
|
|
ops_of(wrapped ? scr_union_retain_v : scr_arr_retain_v,
|
|
wrapped ? scr_union_release_v : scr_arr_release_v,
|
|
wrapped ? scr_union_trace_v : scr_arr_trace_v,
|
|
mode == 2 ? scr_arr_retain_v : scr_str_retain_v,
|
|
mode == 2 ? scr_arr_release_v : scr_str_release_v, value_trace, mode == 4 ? union_kinds : NULL));
|
|
ScrUnion *box = wrapped ? wrap_key(0, key, scr_arr_trace_v) : NULL;
|
|
void *stored_key = wrapped ? (void *)box : (void *)key;
|
|
if (mode == 0) scr_map_set_ref_f64(m, stored_key, i);
|
|
else if (mode == 2) scr_map_set_ref_ref(m, stored_key, scr_arr_retain(key));
|
|
else scr_map_set_ref_ref(m, stored_key, S("untraced value must be freed"));
|
|
scr_arr_push_ref(key, scr_map_retain(m));
|
|
if (box) scr_union_release(box);
|
|
scr_arr_release(key);
|
|
scr_map_release(m);
|
|
}
|
|
scr_collect_cycles();
|
|
check(scr_map_live_count() == maps, "key-cycle maps collected");
|
|
check(scr_arr_live_count() == arrays, "key-cycle payload arrays collected");
|
|
check(scr_str_live_count() == strings, "collector releases untraced values beside traced keys");
|
|
check(scr_union_live_count() == unions, "key-cycle union wrappers collected");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* The absent-key filter: every live entry's hash bits stay in m->filter
|
|
* through appends, overwrites, deletes, compaction, growth, clears (also
|
|
* during an iteration), direct-index transitions and clones, so a filtered
|
|
* miss is always a true miss. A seeded model checks membership of a key
|
|
* universe after every operation, for string, number and reference keys. */
|
|
static uint64_t filter_bits(uint64_t hash) {
|
|
return (UINT64_C(1) << (hash >> 44 & 63)) | (UINT64_C(1) << (hash >> 50 & 63));
|
|
}
|
|
|
|
static bool filter_covers_live(const ScrMap *m) {
|
|
for (size_t e = 0; e < m->nentries; e++) {
|
|
uint64_t h = m->entries[e].hash;
|
|
if (h && (m->filter & filter_bits(h)) != filter_bits(h)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void *filter_ref_retain(void *p) { return p; }
|
|
static void filter_ref_release(void *p) { (void)p; }
|
|
|
|
static uint32_t filter_rng = 12345;
|
|
static uint32_t filter_next(void) {
|
|
filter_rng = filter_rng * 1103515245u + 12345u;
|
|
return filter_rng >> 8;
|
|
}
|
|
|
|
static void test_absent_key_filter(void) {
|
|
enum { UNIVERSE = 600 };
|
|
ScrStr *names[UNIVERSE];
|
|
static int refs[UNIVERSE];
|
|
char buf[32];
|
|
for (int i = 0; i < UNIVERSE; i++) {
|
|
snprintf(buf, sizeof buf, "k%d", i * 7919);
|
|
names[i] = S(buf);
|
|
}
|
|
ScrMap *ms = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
|
ScrMap *mf = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL);
|
|
ScrMap *mr = scr_map_new(SCR_MAP_KEY_REF, SCR_MAP_VAL_F64,
|
|
ops_of(filter_ref_retain, filter_ref_release, NULL, NULL, NULL, NULL, NULL));
|
|
static bool in_s[UNIVERSE], in_f[UNIVERSE], in_r[UNIVERSE];
|
|
memset(in_s, 0, sizeof in_s);
|
|
memset(in_f, 0, sizeof in_f);
|
|
memset(in_r, 0, sizeof in_r);
|
|
bool ok = true, covered = true;
|
|
int iterating = 0;
|
|
for (int step = 0; step < 40000; step++) {
|
|
uint32_t r = filter_next();
|
|
/* Phases: grow to a few hundred keys, then shrink, repeatedly, so the
|
|
* maps pass through linear, bucketed and direct-index storage. */
|
|
int span = (step / 4000) % 2 ? 40 : UNIVERSE;
|
|
int i = (int)(r % (uint32_t)span);
|
|
/* Number keys alternate between dense indices and sparse values. */
|
|
double fk = (step / 8000) % 2 ? (double)i : (double)i * 1000.5;
|
|
switch ((r >> 20) % 16) {
|
|
case 0: case 1: case 2: case 3: case 4: case 5:
|
|
scr_map_set_str_f64(ms, names[i], (double)step);
|
|
in_s[i] = true;
|
|
scr_map_set_f64_f64(mf, fk, (double)step);
|
|
in_f[i] = true;
|
|
scr_map_set_ref_f64(mr, &refs[i], 0);
|
|
in_r[i] = true;
|
|
break;
|
|
case 6: case 7: case 8: case 9:
|
|
ok &= scr_map_delete_str(ms, names[i]) == in_s[i];
|
|
in_s[i] = false;
|
|
ok &= scr_map_delete_f64(mf, fk) == in_f[i];
|
|
in_f[i] = false;
|
|
ok &= scr_map_delete_ref(mr, &refs[i]) == in_r[i];
|
|
in_r[i] = false;
|
|
break;
|
|
case 10:
|
|
if ((r >> 4) % 64 == 0) {
|
|
scr_map_clear(ms);
|
|
scr_map_clear(mf);
|
|
scr_map_clear(mr);
|
|
memset(in_s, 0, sizeof in_s);
|
|
memset(in_f, 0, sizeof in_f);
|
|
memset(in_r, 0, sizeof in_r);
|
|
}
|
|
break;
|
|
case 11:
|
|
/* Hold or release an iteration: growth keeps tombstones meanwhile. */
|
|
if (!iterating) {
|
|
scr_map_iter_enter(ms);
|
|
scr_map_iter_enter(mf);
|
|
scr_map_iter_enter(mr);
|
|
iterating = 1;
|
|
} else {
|
|
scr_map_iter_exit(ms);
|
|
scr_map_iter_exit(mf);
|
|
scr_map_iter_exit(mr);
|
|
iterating = 0;
|
|
}
|
|
break;
|
|
case 12: {
|
|
ScrMap *copy = scr_map_clone(ms, false, NULL);
|
|
covered &= filter_covers_live(copy);
|
|
for (int j = 0; j < span; j++) ok &= scr_map_has_str(copy, names[j]) == in_s[j];
|
|
scr_map_release(copy);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
covered &= filter_covers_live(ms) && filter_covers_live(mf) && filter_covers_live(mr);
|
|
if (step % 97 == 0) {
|
|
for (int j = 0; j < UNIVERSE; j++) {
|
|
double fj = (step / 8000) % 2 ? (double)j : (double)j * 1000.5;
|
|
ok &= scr_map_has_str(ms, names[j]) == in_s[j];
|
|
ScrStr *probe = scr_str_new(names[j]->data, names[j]->len); /* uncached hash */
|
|
ok &= (scr_map_entry_str(ms, probe) != NULL) == in_s[j];
|
|
scr_str_release(probe);
|
|
ok &= scr_map_has_f64(mf, fj) == in_f[j];
|
|
ok &= scr_map_has_ref(mr, &refs[j]) == in_r[j];
|
|
}
|
|
}
|
|
/* A number-key phase change re-keys the model: drop everything. */
|
|
if (step % 8000 == 7999) {
|
|
scr_map_clear(mf);
|
|
memset(in_f, 0, sizeof in_f);
|
|
}
|
|
}
|
|
if (iterating) {
|
|
scr_map_iter_exit(ms);
|
|
scr_map_iter_exit(mf);
|
|
scr_map_iter_exit(mr);
|
|
}
|
|
check(covered, "filter keeps every live entry's bits");
|
|
check(ok, "filtered lookups agree with the model");
|
|
/* A cleared map's filter is empty, and refills cover new keys. */
|
|
scr_map_clear(ms);
|
|
check(ms->filter == 0, "clear empties the filter");
|
|
scr_map_set_str_f64(ms, names[3], 1);
|
|
check(scr_map_has_str(ms, names[3]) && !scr_map_has_str(ms, names[4]), "refill after clear");
|
|
scr_map_release(ms);
|
|
scr_map_release(mf);
|
|
scr_map_release(mr);
|
|
for (int i = 0; i < UNIVERSE; i++) scr_str_release(names[i]);
|
|
}
|
|
|
|
int main(void) {
|
|
test_key_bytes();
|
|
test_string_bucket_collisions();
|
|
test_string_keys();
|
|
test_string_entries();
|
|
test_same_value_zero();
|
|
test_span_keys();
|
|
test_scratch_keys();
|
|
test_string_hash_cache();
|
|
#ifdef SCR_RC_AUDIT
|
|
test_rc_accounting();
|
|
#endif
|
|
test_churn_stays_bounded();
|
|
test_live_iteration_indices();
|
|
test_small_storage_transitions();
|
|
test_identity_key_ownership();
|
|
test_identity_scalar_values();
|
|
test_reboxed_keys_and_rehash();
|
|
test_value_keys_and_copies();
|
|
test_absent_key_filter();
|
|
#ifdef SCR_RC_AUDIT
|
|
test_identity_cycles();
|
|
scr_collect_cycles();
|
|
check(scr_map_live_count() == 0, "all maps reclaimed");
|
|
check(scr_arr_live_count() == 0, "all key arrays reclaimed");
|
|
check(scr_str_live_count() == 0, "all owned strings reclaimed");
|
|
check(scr_union_live_count() == 0, "all key wrappers reclaimed");
|
|
#endif
|
|
|
|
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
|
|
return failed == 0 ? 0 : 1;
|
|
}
|