/* 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 #include #include #include #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; }