/* Unit tests for the array runtime (scr_array.c). Built with ASan + * -DSCR_RC_AUDIT by array.test.ts, which also asserts the trap modes abort: * * (no args) run all assertions; prints "N/N cases passed" * --crash-get-oob read past the end → RangeError + abort() * --crash-get-frac read a fractional index → RangeError + abort() * --crash-hole-read read a hole → RangeError + abort() * --crash-pop-empty pop an empty array → RangeError + abort() * * The RC-recursion cases (array of strings, array of arrays of strings) * assert live counts directly: releasing the outer array must release * every reachable element exactly once. */ #include "../src/scr_runtime.h" #include #include #include #include /* Sparse entries live in the array's out-of-line storage. */ static size_t arr_sparse_len(const ScrArr *a) { return a->ext ? a->ext->sparse_len : 0; } #ifdef SCR_RC_AUDIT long scr_str_live_count(void); /* provided by scr_string.c */ long scr_arr_live_count(void); /* provided by scr_array.c */ #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 void check_f64(double got, double want, const char *what) { check(got == want, what); if (got != want) fprintf(stderr, " got %g want %g\n", got, want); } static void test_f64_basics(void) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); check_f64(scr_arr_len(a), 0, "new array is empty"); check_f64(scr_arr_push_f64(a, 1.5), 1, "push returns new length"); check_f64(scr_arr_push_f64(a, -0.0), 2, "push returns new length (2)"); check_f64(scr_arr_get_f64(a, 0), 1.5, "get_f64[0]"); check(scr_arr_get_f64(a, 1) == 0 && signbit(scr_arr_get_f64(a, 1)), "-0 round-trips through a slot"); scr_arr_set_f64(a, 0, 7); check_f64(scr_arr_get_f64(a, 0), 7, "set_f64 replaces"); scr_arr_set_f64(a, 2, 9); /* i == len appends */ check_f64(scr_arr_len(a), 3, "set at len appends"); check_f64(scr_arr_get_f64(a, 2), 9, "appended value readable"); check_f64(scr_arr_pop_f64(a), 9, "pop returns last"); check_f64(scr_arr_len(a), 2, "pop shrinks"); /* NaN round-trips bit-exactly through the uint64_t slot. */ scr_arr_push_f64(a, 0.0 / 0.0); check(scr_arr_get_f64(a, 2) != scr_arr_get_f64(a, 2), "NaN round-trips"); /* growth across many appends */ for (double i = 0; i < 1000; i++) scr_arr_push_f64(a, i); check_f64(scr_arr_len(a), 1003, "1000 pushes grow"); check_f64(scr_arr_get_f64(a, 1002), 999, "last survives growth"); check_f64(scr_arr_get_f64(a, 0), 7, "first survives growth"); scr_arr_release(a); } static void test_numeric_read(void) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); check(isnan(scr_arr_get_number(a, 0)), "numeric read of empty array"); scr_arr_set_f64(a, 0, -0.0); check(signbit(scr_arr_get_number(a, -0.0)), "numeric read preserves signed zero"); scr_arr_set_f64(a, 3, INFINITY); check(isnan(scr_arr_get_number(a, 1)), "numeric read of dense hole"); check_f64(scr_arr_get_number(a, 3), INFINITY, "numeric read of infinity"); scr_arr_set_undefined(a, 2); check(isnan(scr_arr_get_number(a, 2)) && scr_arr_has(a, 2), "numeric read of present undefined keeps presence"); scr_arr_set_f64(a, 4294967294.0, -INFINITY); check_f64(scr_arr_get_number(a, 4294967294.0), -INFINITY, "numeric read of last sparse array index"); check(isnan(scr_arr_get_number(a, 4294967293.0)), "numeric read of sparse hole"); scr_arr_set_undefined(a, 4294967294.0); check(isnan(scr_arr_get_number(a, 4294967294.0)), "numeric read of sparse undefined"); double keys[] = {-1, 0.5, 4294967295.0, NAN, INFINITY, -INFINITY}; for (size_t i = 0; i < sizeof keys / sizeof *keys; i++) { check(isnan(scr_arr_get_number(a, keys[i])), "numeric read of missing property"); scr_arr_set_f64(a, keys[i], (double)i + 10); check_f64(scr_arr_get_number(a, keys[i]), (double)i + 10, "numeric read of ordinary numeric property"); scr_arr_set_undefined(a, keys[i]); check(isnan(scr_arr_get_number(a, keys[i])) && scr_arr_has(a, keys[i]), "numeric read of undefined numeric property"); } scr_arr_set_f64(a, 0, NAN); check(isnan(scr_arr_get_number(a, 0)), "numeric read of stored NaN"); scr_arr_release(a); } static void test_bool(void) { ScrArr *a = scr_arr_new(SCR_ELEM_BOOL, 2); scr_arr_push_bool(a, true); scr_arr_push_bool(a, false); check(scr_arr_get_bool(a, 0) == true, "get_bool true"); check(scr_arr_get_bool(a, 1) == false, "get_bool false"); scr_arr_set_bool(a, 0, false); check(scr_arr_get_bool(a, 0) == false, "set_bool replaces"); check(scr_arr_pop_bool(a) == false, "pop_bool"); scr_arr_release(a); } static void test_dense_replacement(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); #endif ScrArr *a = scr_arr_new(SCR_ELEM_STR, 32); scr_arr_set_ref(a, 7, scr_str_new("first", 5)); check(scr_arr_len(a) == 8 && !scr_arr_has(a, 6), "write within capacity grows length without filling holes"); /* Self-assignment moves the getter's retained reference back in. */ scr_arr_set_ref(a, 7, scr_arr_get_ref(a, 7)); ScrStr *s = scr_arr_get_ref(a, 7); check(s->rc == 2 && strcmp(s->data, "first") == 0, "self replacement preserves exactly one stored reference"); scr_str_release(s); scr_arr_set_undefined(a, 7); scr_arr_set_ref(a, 7, scr_str_new("second", 6)); scr_arr_delete(a, 7); scr_arr_set_ref(a, 7, scr_str_new("third", 5)); scr_arr_set_len(a, 0); scr_arr_set_ref(a, 9, scr_str_new("last", 4)); check(!scr_arr_has(a, 7) && scr_arr_len(a) == 10, "refill after truncation does not resurrect old entries"); s = scr_arr_get_ref(a, 9); check(strcmp(s->data, "last") == 0, "refill after truncation owns new value"); scr_str_release(s); scr_arr_release(a); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "dense replacements release every string"); #endif } static void test_unshift_reverse(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); long arrays0 = scr_arr_live_count(); #endif /* The emitter applies variadic unshift arguments right-to-left. */ ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(a, 3); scr_arr_push_f64(a, 4); check_f64(scr_arr_unshift_f64(a, 2), 3, "unshift f64 grows"); check_f64(scr_arr_unshift_f64(a, 1), 4, "variadic-style unshift length"); check_f64(scr_arr_get_f64(a, 0), 1, "unshift preserves arg order [0]"); check_f64(scr_arr_get_f64(a, 1), 2, "unshift preserves arg order [1]"); ScrArr *same = scr_arr_reverse(a); check(same == a, "reverse returns receiver identity"); check_f64(scr_arr_get_f64(a, 0), 4, "reverse mutates first slot"); check_f64(scr_arr_get_f64(a, 3), 1, "reverse mutates last slot"); scr_arr_release(same); /* reverse's returned +1 */ ScrArr *front = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(front, 8); scr_arr_push_f64(front, 9); check_f64(scr_arr_unshift_spread(a, front), 6, "unshift spread length"); check_f64(scr_arr_get_f64(a, 0), 8, "unshift spread first"); check_f64(scr_arr_get_f64(a, 1), 9, "unshift spread second"); scr_arr_release(front); scr_arr_release(a); /* Self-spread snapshots the original block before front insertion. */ ScrArr *self = scr_arr_new(SCR_ELEM_BOOL, 0); scr_arr_push_bool(self, true); scr_arr_push_bool(self, false); check_f64(scr_arr_unshift_spread(self, self), 4, "unshift self-spread length"); check(scr_arr_get_bool(self, 0) && !scr_arr_get_bool(self, 1), "unshift self-spread copied prefix"); check(scr_arr_get_bool(self, 2) && !scr_arr_get_bool(self, 3), "unshift self-spread kept original tail"); scr_arr_release(self); /* Spread retains ref elements; either array may then die first. */ ScrArr *src = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_push_ref(src, scr_str_new("front", 5)); ScrArr *dst = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_push_ref(dst, scr_str_new("back", 4)); scr_arr_unshift_spread(dst, src); ScrStr *copied = (ScrStr *)scr_arr_get_ref(dst, 0); check(copied->rc == 3, "unshift spread retained ref element"); scr_str_release(copied); scr_arr_release(src); ScrStr *still = (ScrStr *)scr_arr_get_ref(dst, 0); check(strcmp(still->data, "front") == 0, "unshift spread ref survives source release"); scr_str_release(still); scr_arr_release(dst); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "unshift/reverse: no strings leaked"); check(scr_arr_live_count() == arrays0, "unshift/reverse: no arrays leaked"); #endif } static void test_str_rc(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); long arrays0 = scr_arr_live_count(); #endif ScrArr *a = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_push_ref(a, scr_str_new("one", 3)); /* ownership moves in */ scr_arr_push_ref(a, scr_str_new("two", 3)); /* get_ref returns +1: the array and the caller each own a reference. */ ScrStr *s = (ScrStr *)scr_arr_get_ref(a, 0); check(s->rc == 2, "get_ref retained"); check(strcmp(s->data, "one") == 0, "get_ref content"); scr_str_release(s); /* set_ref releases the replaced element and owns the new one. */ scr_arr_set_ref(a, 0, scr_str_new("uno", 3)); ScrStr *r = (ScrStr *)scr_arr_get_ref(a, 0); check(strcmp(r->data, "uno") == 0, "set_ref replaced content"); scr_str_release(r); /* pop_ref transfers ownership out: rc unchanged, array no longer owns. */ ScrStr *popped = (ScrStr *)scr_arr_pop_ref(a); check(popped->rc == 1, "pop_ref transfers ownership"); check(strcmp(popped->data, "two") == 0, "pop_ref content"); scr_str_release(popped); /* immortal literals in arrays: retain/release must stay no-ops */ static struct { size_t rc; size_t len; size_t cap; char data[4]; } lit = {SIZE_MAX, 3, 3, "lit"}; scr_arr_push_ref(a, (ScrStr *)&lit); ScrStr *l = (ScrStr *)scr_arr_get_ref(a, 1); check(l->rc == SIZE_MAX, "immortal element stays immortal"); scr_arr_release(a); /* must release "uno" (and skip the literal) */ #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "no strings leaked"); check(scr_arr_live_count() == arrays0, "no arrays leaked"); #endif } static void test_nested_rc(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); long arrays0 = scr_arr_live_count(); #endif /* [[ "a" ], [ "b", "c" ]] — releasing the outer array must cascade. */ ScrArr *outer = scr_arr_new(SCR_ELEM_ARR, 0); ScrArr *row0 = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_push_ref(row0, scr_str_new("a", 1)); scr_arr_push_ref(outer, row0); ScrArr *row1 = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_push_ref(row1, scr_str_new("b", 1)); scr_arr_push_ref(row1, scr_str_new("c", 1)); scr_arr_push_ref(outer, row1); ScrArr *row = (ScrArr *)scr_arr_get_ref(outer, 1); check(row->rc == 2, "nested get_ref retained"); check_f64(scr_arr_len(row), 2, "inner length"); scr_arr_release(row); scr_arr_release(outer); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "nested: no strings leaked"); check(scr_arr_live_count() == arrays0, "nested: no arrays leaked"); #endif } static void test_index_of_includes(void) { /* f64: indexOf is strict equality (NaN never matches, -0 == 0); * includes is SameValueZero (NaN matches NaN). */ ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(a, 1); scr_arr_push_f64(a, 0.0 / 0.0); scr_arr_push_f64(a, -0.0); check_f64(scr_arr_index_of_f64(a, 1), 0, "indexOf f64 hit"); check_f64(scr_arr_index_of_f64(a, 2), -1, "indexOf f64 miss"); check_f64(scr_arr_index_of_f64(a, 0.0 / 0.0), -1, "indexOf NaN never matches"); check(scr_arr_includes_f64(a, 0.0 / 0.0), "includes NaN matches NaN"); check_f64(scr_arr_index_of_f64(a, 0.0), 2, "indexOf 0 matches -0"); check(scr_arr_includes_f64(a, 0.0), "includes 0 matches -0"); check(!scr_arr_includes_f64(a, 5), "includes miss"); scr_arr_release(a); /* bool by value. */ ScrArr *b = scr_arr_new(SCR_ELEM_BOOL, 0); scr_arr_push_bool(b, false); scr_arr_push_bool(b, true); check_f64(scr_arr_index_of_bool(b, true), 1, "indexOf bool"); check(scr_arr_includes_bool(b, false), "includes bool"); scr_arr_release(b); /* strings by CONTENT; needle borrowed (rc unchanged, released by us). */ ScrArr *s = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_push_ref(s, scr_str_new("aa", 2)); scr_arr_push_ref(s, scr_str_new("bb", 2)); ScrStr *needle = scr_str_new("bb", 2); check_f64(scr_arr_index_of_ref(s, needle), 1, "indexOf string by content"); check(scr_arr_includes_ref(s, needle), "includes string by content"); check(needle->rc == 1, "indexOf/includes borrow the needle"); scr_str_release(needle); scr_arr_release(s); /* nested arrays by reference identity. */ ScrArr *outer = scr_arr_new(SCR_ELEM_ARR, 0); ScrArr *inner = scr_arr_new(SCR_ELEM_F64, 0); ScrArr *other = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_ref(outer, scr_arr_retain(inner)); check_f64(scr_arr_index_of_ref(outer, inner), 0, "indexOf array by identity"); check_f64(scr_arr_index_of_ref(outer, other), -1, "structurally-equal array misses"); scr_arr_release(inner); scr_arr_release(other); scr_arr_release(outer); } /* ── SCR_ELEM_REF: a mock "record" the runtime cannot lay out ───────── * Acyclic flavor: 1-word rc header, retain/release via the stored fn ptrs * (the compiler's `_v` adapters stand in). Cyclic flavor: allocated with a * collector header whose trace visits an owner ARRAY slot — the record can * point back at the array holding it, the REF cycle case. */ typedef struct MockRec { size_t rc; int value; ScrArr *owner; /* cyclic flavor only: traced edge back at an array */ } MockRec; static long mock_live = 0; static void *mock_retain(void *p) { MockRec *r = (MockRec *)p; if (r->rc != SIZE_MAX) r->rc++; return r; } static void mock_release(void *p) { MockRec *r = (MockRec *)p; if (!r || r->rc == SIZE_MAX) return; if (--r->rc == 0) { mock_live--; free(r); } } static MockRec *mock_new(int value) { MockRec *r = calloc(1, sizeof *r); r->rc = 1; r->value = value; mock_live++; return r; } static void mock_trace(void *p, ScrTraceVisit visit, void *ctx) { MockRec *r = (MockRec *)p; if (r->owner) visit(r->owner, ctx); } static void mock_gcfree(void *p) { /* trace visits owner (headered); nothing else refcounted to release */ mock_live--; scr_cyc_free(p); } static void *mock_cyc_retain(void *p) { MockRec *r = (MockRec *)p; if (r->rc != SIZE_MAX) { r->rc++; } return r; } static void mock_cyc_release(void *p) { MockRec *r = (MockRec *)p; if (!r || r->rc == SIZE_MAX) return; if (--r->rc == 0) { scr_cyc_on_dead(r); if (r->owner) scr_arr_release(r->owner); mock_live--; scr_cyc_free(r); } else { scr_cyc_on_release(r); } } static MockRec *mock_cyc_new(int value) { static ScrCycType cyc_type = { &mock_trace, &mock_gcfree, 0 }; MockRec *r = scr_cyc_alloc(sizeof *r, &cyc_type); r->rc = 1; r->value = value; mock_live++; return r; } static const ScrArrOps mock_ops = { &mock_retain, &mock_release, NULL }; static const ScrArrOps mock_cyc_ops = { &mock_cyc_retain, &mock_cyc_release, &mock_trace }; static void test_ref_elements(void) { #ifdef SCR_RC_AUDIT long arrays0 = scr_arr_live_count(); #endif ScrArr *a = scr_arr_new_ops(&mock_ops, 0); scr_arr_push_ref(a, mock_new(1)); /* ownership moves in */ scr_arr_push_ref(a, mock_new(2)); check_f64(scr_arr_len(a), 2, "ref push grows"); /* get_ref retains through the stored fn ptr. */ MockRec *r = (MockRec *)scr_arr_get_ref(a, 0); check(r->rc == 2, "ref get_ref retained via elem_retain"); check(r->value == 1, "ref get_ref content"); mock_release(r); /* set_ref releases the replaced element through elem_release. */ scr_arr_set_ref(a, 0, mock_new(10)); check(mock_live == 2, "ref set_ref released the old element"); MockRec *rr = (MockRec *)scr_arr_get_ref(a, 0); check(rr->value == 10, "ref set_ref replaced content"); mock_release(rr); /* indexOf/includes: POINTER identity, needle borrowed. */ MockRec *second = (MockRec *)scr_arr_get_ref(a, 1); check_f64(scr_arr_index_of_ref(a, second), 1, "ref indexOf by identity"); check(scr_arr_includes_ref(a, second), "ref includes by identity"); MockRec *stranger = mock_new(2); check_f64(scr_arr_index_of_ref(a, stranger), -1, "ref equal-value stranger misses"); check(second->rc == 2, "ref indexOf borrows the needle"); mock_release(stranger); /* pop_ref transfers ownership out. */ MockRec *popped = (MockRec *)scr_arr_pop_ref(a); check(popped == second, "ref pop_ref returns the element"); check(popped->rc == 2, "ref pop_ref transfers (our get_ref + the pop)"); mock_release(popped); mock_release(second); scr_arr_release(a); /* releases the remaining element */ check(mock_live == 0, "ref elements all released"); #ifdef SCR_RC_AUDIT check(scr_arr_live_count() == arrays0, "ref: no arrays leaked"); #endif } static void test_ref_cycle(void) { #ifdef SCR_RC_AUDIT long arrays0 = scr_arr_live_count(); #endif /* arr -> rec -> arr: drop the external references, then collect. */ ScrArr *arr = scr_arr_new_ops(&mock_cyc_ops, 0); MockRec *rec = mock_cyc_new(7); rec->owner = (ScrArr *)scr_arr_retain(arr); /* rec points back at arr */ scr_arr_push_ref(arr, rec); /* arr owns rec */ check(mock_live == 1, "cycle: element alive"); scr_arr_release(arr); /* external edge gone; the cycle keeps both alive */ scr_collect_cycles(); check(mock_live == 0, "cycle collected through the array element"); #ifdef SCR_RC_AUDIT check(scr_arr_live_count() == arrays0, "cycle: no arrays leaked"); #endif } static void test_ref_truncate_cycle(void) { #ifdef SCR_RC_AUDIT long arrays0 = scr_arr_live_count(); #endif /* The truncation release runs while the record points back at the array. * The array edge must be detached before mock_cyc_release can trigger the * cycle collector. */ ScrArr *arr = scr_arr_new_ops(&mock_cyc_ops, 0); MockRec *rec = mock_cyc_new(8); rec->owner = (ScrArr *)scr_arr_retain(arr); scr_arr_push_ref(arr, rec); scr_arr_set_len(arr, 0); scr_collect_cycles(); check(mock_live == 0, "truncation detaches cyclic ref before release"); scr_arr_release(arr); #ifdef SCR_RC_AUDIT check(scr_arr_live_count() == arrays0, "truncation cycle: no arrays leaked"); #endif } static void test_ref_trace_storage_boundaries(void) { long before = mock_live; ScrArr *arr = scr_arr_new_ops(&mock_cyc_ops, 4096); MockRec *dense = mock_cyc_new(1); MockRec *sparse = mock_cyc_new(2); MockRec *property = mock_cyc_new(3); dense->owner = scr_arr_retain(arr); sparse->owner = scr_arr_retain(arr); property->owner = scr_arr_retain(arr); scr_arr_set_ref(arr, 0, dense); scr_arr_set_ref(arr, 3, mock_cyc_retain(dense)); scr_arr_set_undefined(arr, 2); scr_arr_set_ref(arr, 4294967294.0, sparse); scr_arr_set_ref(arr, -1, property); scr_collect_cycles(); check(mock_live == before + 3, "trace preserves dense, sparse and property edges"); check(dense->rc == 2, "trace restores duplicate dense edges exactly"); scr_arr_set_len(arr, 1); scr_collect_cycles(); check(mock_live == before + 2, "truncation releases sparse and duplicate edges"); check(dense->rc == 1 && property->rc == 1, "short array preserves its element and named property"); scr_arr_set_len(arr, 0); scr_collect_cycles(); check(mock_live == before + 1, "zero length keeps the named property"); scr_arr_release(arr); scr_collect_cycles(); check(mock_live == before, "empty reserved storage and property cycle collected"); } static void test_join(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); #endif ScrStr *sep = scr_str_new(",", 1); ScrArr *n = scr_arr_new(SCR_ELEM_F64, 0); ScrStr *empty = scr_arr_join(n, sep); check(empty->len == 0, "join of empty array is \"\""); scr_str_release(empty); scr_arr_push_f64(n, 1.5); scr_arr_push_f64(n, -0.0); scr_arr_push_f64(n, 0.0 / 0.0); ScrStr *nums = scr_arr_join(n, sep); check(strcmp(nums->data, "1.5,0,NaN") == 0, "join f64 (JS formatting, -0 -> \"0\")"); scr_str_release(nums); scr_arr_release(n); ScrArr *b = scr_arr_new(SCR_ELEM_BOOL, 0); scr_arr_push_bool(b, true); scr_arr_push_bool(b, false); ScrStr *bools = scr_arr_join(b, sep); check(strcmp(bools->data, "true,false") == 0, "join bool"); scr_str_release(bools); scr_arr_release(b); ScrArr *s = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_push_ref(s, scr_str_new("a", 1)); scr_arr_push_ref(s, scr_str_new("", 0)); scr_arr_push_ref(s, scr_str_new("c", 1)); ScrStr *sep0 = scr_str_new("", 0); ScrStr *sepless = scr_arr_join(s, sep0); /* join borrows the separator */ check(strcmp(sepless->data, "ac") == 0, "join with empty separator"); scr_str_release(sepless); scr_str_release(sep0); ScrStr *sep2 = scr_str_new("--", 2); ScrStr *strs = scr_arr_join(s, sep2); check(strcmp(strs->data, "a----c") == 0, "join strings verbatim, empty kept"); scr_str_release(strs); scr_str_release(sep2); scr_arr_release(s); s = scr_arr_new(SCR_ELEM_STR, 1); ScrStr *shared = scr_str_new("x\0y", 3); scr_arr_set_ref(s, 0, scr_str_retain(shared)); scr_arr_set_undefined(s, 1); scr_arr_set_ref(s, 8192, scr_str_retain(shared)); scr_arr_set_ref(s, -1, scr_str_new("ignored", 7)); sep0 = scr_str_new("", 0); strs = scr_arr_join(s, sep0); check(strs->len == 6 && memcmp(strs->data, "x\0yx\0y", 6) == 0, "join handles sparse values and embedded zero without properties"); check(shared->rc == 3 && strs->data[strs->len] == '\0', "join preserves duplicate input owners and terminates final storage"); scr_str_release(strs); strs = scr_arr_join(s, sep); check(strs->len == 8198 && strs->data[3] == ',' && strs->data[8195] == 'x', "join sizing includes separators for sparse holes and undefined"); scr_str_release(strs); scr_arr_release(s); scr_str_release(shared); scr_str_release(sep0); n = scr_arr_new(SCR_ELEM_F64, 0); for (size_t i = 0; i < 300; i++) scr_arr_push_f64(n, 123.5); nums = scr_arr_join(n, sep); check(nums->len == 1799 && nums->data[1799] == '\0' && memcmp(nums->data + 1794, "123.5", 5) == 0, "numeric join grows final storage without losing its suffix"); scr_str_release(nums); scr_arr_release(n); scr_str_release(sep); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "join: no strings leaked"); #endif } static void test_sparse_holes(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); long arrays0 = scr_arr_live_count(); #endif ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_set_f64(a, 3, 7); check_f64(scr_arr_len(a), 4, "far indexed write grows length"); check(!scr_arr_has(a, 0) && !scr_arr_has(a, 2), "growth leaves holes absent"); check(scr_arr_has(a, 3), "far indexed write is present"); check_f64(scr_arr_get_f64(a, 3), 7, "far indexed value is readable"); scr_arr_set_undefined(a, 2); check(scr_arr_state(a, 2) == SCR_ARR_UNDEFINED && scr_arr_has(a, 2), "explicit undefined is present and stateful"); check_f64(scr_arr_next_present(a, 0), 2, "next-present traversal finds explicit undefined"); check_f64(scr_arr_next_present(a, 3), 3, "next-present traversal finds dense values"); check_f64(scr_arr_next_present(a, 4), 4, "next-present traversal stops at length"); check(scr_arr_delete(a, 2) && scr_arr_state(a, 2) == SCR_ARR_HOLE && scr_arr_len(a) == 4, "delete removes a slot without shrinking length"); ScrStr *hole_sep = scr_str_new(",", 1); ScrStr *hole_join = scr_arr_join(a, hole_sep); check(strcmp(hole_join->data, ",,,7") == 0, "join preserves separators for holes"); scr_str_release(hole_join); scr_str_release(hole_sep); ScrArr *slice = scr_arr_slice(a, 0, 4); check_f64(scr_arr_len(slice), 4, "slice keeps sparse length"); check(!scr_arr_has(slice, 0) && scr_arr_has(slice, 3), "slice preserves hole presence"); scr_arr_release(slice); ScrArr *materialized = scr_arr_to_reversed(a); check(scr_arr_state(materialized, 1) == SCR_ARR_UNDEFINED, "toReversed materializes a hole as undefined"); scr_arr_release(materialized); ScrArr *concat = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_concat_copy(concat, a); check(scr_arr_state(concat, 0) == SCR_ARR_HOLE, "concat copy preserves holes"); ScrArr *spread = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_spread(spread, a); check(scr_arr_state(spread, 0) == SCR_ARR_UNDEFINED, "spread materializes holes as undefined"); scr_arr_release(concat); scr_arr_release(spread); ScrArr *rev = scr_arr_reverse(a); check(rev == a, "sparse reverse keeps identity"); check(scr_arr_has(a, 0) && !scr_arr_has(a, 3), "reverse moves presence with the value"); scr_arr_release(rev); scr_arr_release(a); ScrArr *high = scr_arr_new(SCR_ELEM_F64, 0); const double high_index = 4000000000.0; scr_arr_set_f64(high, high_index, 11); check(scr_arr_has(high, high_index), "high sparse index is present"); check(high->cap <= ((size_t)1 << 20), "high sparse index avoids dense allocation"); check(arr_sparse_len(high) == 1, "high sparse index uses side storage"); check_f64(scr_arr_next_present(high, 0), high_index, "next-present traversal jumps to side storage"); scr_arr_set_len(high, 0); check(arr_sparse_len(high) == 0 && scr_arr_len(high) == 0, "length shrink removes sparse entries"); scr_arr_release(high); ScrArr *refs = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_set_ref(refs, high_index, scr_str_new("sparse", 6)); scr_arr_set_len(refs, 0); scr_arr_release(refs); ScrArr *props = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_set_f64(props, -1, 21); scr_arr_set_f64(props, 0.5, 22); scr_arr_set_f64(props, 4294967295.0, 23); check_f64(scr_arr_len(props), 0, "ordinary numeric properties do not grow length"); check(scr_arr_has(props, -1) && scr_arr_has(props, 0.5) && scr_arr_has(props, 4294967295.0), "noncanonical numeric properties are present"); check_f64(scr_arr_get_f64(props, -1), 21, "negative numeric property round-trips"); check_f64(scr_arr_get_f64(props, 0.5), 22, "fractional numeric property round-trips"); check_f64(scr_arr_get_f64(props, 4294967295.0), 23, "max non-index numeric property round-trips"); scr_arr_set_undefined(props, -2); scr_arr_set_undefined(props, 1.5); scr_arr_set_undefined(props, 4294967295.0); check(scr_arr_state(props, -2) == SCR_ARR_UNDEFINED && scr_arr_state(props, 1.5) == SCR_ARR_UNDEFINED && scr_arr_state(props, 4294967295.0) == SCR_ARR_UNDEFINED, "ordinary undefined properties retain their state"); check(scr_arr_has(props, -2) && scr_arr_has(props, 1.5) && scr_arr_has(props, 4294967295.0), "ordinary undefined properties remain present"); scr_arr_set_len(props, 0); check(scr_arr_has(props, -1), "length truncation leaves ordinary properties"); scr_arr_release(props); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "sparse ref truncation releases values"); check(scr_arr_live_count() == arrays0, "sparse arrays do not leak"); #endif } /* Arrays beyond the 2^20 dense cutoff: contiguous growth and bulk writes stay * packed, rewrites of sparse entries happen in place, a populated sparse tail * densifies (moving its states and references), and genuinely sparse indices * keep side storage. */ static void test_large_storage(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); long arrays0 = scr_arr_live_count(); #endif const size_t cutoff = (size_t)1 << 20; const double n = (double)cutoff + 200000; ScrArr *pushed = scr_arr_new(SCR_ELEM_F64, 0); for (double i = 0; i < n; i++) scr_arr_push_f64(pushed, i); check(arr_sparse_len(pushed) == 0 && pushed->cap >= (size_t)n, "appends past the cutoff stay dense"); check_f64(scr_arr_get_f64(pushed, n - 1), n - 1, "large append round-trips"); scr_arr_release(pushed); ScrArr *filled = scr_arr_new(SCR_ELEM_BOOL, 0); scr_arr_set_len(filled, n); scr_arr_release(scr_arr_fill_bool(filled, true, 0, INFINITY)); check(arr_sparse_len(filled) == 0 && filled->cap >= (size_t)n, "fill of a large holey array allocates dense storage"); scr_arr_set_bool(filled, n - 1, false); check(!scr_arr_get_bool(filled, n - 1) && scr_arr_get_bool(filled, n - 2), "large filled array accepts indexed writes"); scr_arr_release(filled); ScrArr *forward = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_set_len(forward, n); for (double i = 0; i < n; i++) scr_arr_set_f64(forward, i, i * 2); check(arr_sparse_len(forward) == 0, "forward indexed writes extend dense storage"); scr_arr_release(forward); /* Every 64th index past the cutoff is too sparse for packed slots, even * after a full dense prefix, once it starts beyond the append gap. */ ScrArr *strided = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_set_len(strided, (double)cutoff); scr_arr_release(scr_arr_fill_f64(strided, 0, 0, INFINITY)); size_t entries = 0; for (double i = (double)cutoff + 4095; i < n; i += 64, entries++) scr_arr_set_f64(strided, i, i); check(strided->cap == cutoff && arr_sparse_len(strided) == entries, "strided indices past the cutoff keep side storage"); for (int round = 0; round < 3; round++) { for (double i = (double)cutoff + 4095; i < n; i += 64) { scr_arr_set_f64(strided, i, scr_arr_get_f64(strided, i) + 1); } } check(arr_sparse_len(strided) == entries && scr_arr_get_f64(strided, (double)cutoff + 4159) == (double)cutoff + 4162, "rewriting sparse entries updates them in place"); check(!scr_arr_has(strided, (double)cutoff + 1) && scr_arr_len(strided) > (double)cutoff + 4095, "side storage keeps holes absent"); scr_arr_set_f64(strided, (double)cutoff + 1000, 1); check(strided->cap > cutoff + 1000 && scr_arr_has(strided, (double)cutoff + 1000) && !scr_arr_has(strided, (double)cutoff + 999), "a write within the gap past a full prefix extends dense storage"); scr_arr_release(strided); /* Writing backwards from the end leaves a populated sorted tail that * densifies; holes, undefined and references move with it. */ ScrArr *backward = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_set_len(backward, n); scr_arr_set_undefined(backward, n - 1); for (double i = n - 2; i >= (double)cutoff; i -= 2) { scr_arr_set_ref(backward, i, scr_str_new("v", 1)); } check(arr_sparse_len(backward) == 0 && backward->cap >= (size_t)n, "a populated sparse tail densifies"); check(scr_arr_state(backward, n - 1) == SCR_ARR_UNDEFINED && scr_arr_state(backward, n - 3) == SCR_ARR_HOLE && scr_arr_state(backward, n - 2) == SCR_ARR_VALUE && scr_arr_state(backward, (double)cutoff) == SCR_ARR_VALUE, "densifying preserves values, holes and undefined"); scr_arr_release(backward); /* The side store is a deque: descending and ascending first touches, * deletes near both ends, and detached storage keep sorted order. */ ScrArr *deque = scr_arr_new(SCR_ELEM_STR, 0); const double middle = 3000000000.0; for (double k = 0; k < 3000; k++) { scr_arr_set_ref(deque, middle - k * 1000, scr_str_new("d", 1)); scr_arr_set_ref(deque, middle + 1 + k * 1000, scr_str_new("a", 1)); } check(arr_sparse_len(deque) == 6000 && deque->cap == 0, "spread first touches stay sparse"); for (double k = 0; k < 3000; k += 3) { scr_arr_delete(deque, middle - k * 1000); scr_arr_delete(deque, middle + 1 + k * 1000); } size_t visited = 0; bool ordered = true; double previous = -1; for (double i = scr_arr_next_present(deque, 0); i < scr_arr_len(deque); i = scr_arr_next_present(deque, i + 1)) { ordered = ordered && i > previous; previous = i; visited++; } check(visited == 4000 && ordered, "deque traversal stays sorted after deletes"); check(!scr_arr_has(deque, middle) && scr_arr_has(deque, middle - 1000) && scr_arr_has(deque, middle + 1001) && !scr_arr_has(deque, middle + 1), "deque deletes remove exactly their entries"); scr_arr_unshift_ref(deque, scr_str_new("u", 1)); check(scr_arr_has(deque, middle - 999) && scr_arr_has(deque, 0), "unshift rebuilds deque storage"); scr_arr_release(scr_arr_reverse(deque)); scr_arr_release(deque); ScrArr *high = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_set_f64(high, 0, 1); scr_arr_set_f64(high, 4294967294.0, 2); ScrArr *holes = scr_arr_slice(high, 1, INFINITY); check(holes->cap <= cutoff && scr_arr_len(holes) == 4294967294.0, "slicing a huge hole run keeps a bounded allocation"); check(arr_sparse_len(holes) == 1 && scr_arr_get_f64(holes, 4294967293.0) == 2, "a sliced hole run keeps its sparse value"); scr_arr_release(holes); scr_arr_release(high); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "large-array references are released"); check(scr_arr_live_count() == arrays0, "large arrays do not leak"); #endif } static void test_borrowed_ref_read(void) { ScrArr *a = scr_arr_new(SCR_ELEM_STR, 0); ScrStr *value = scr_str_new("borrowed", 8); double keys[] = {0, -0.0, 0.5, -1, NAN, INFINITY, -INFINITY, 4294967294.0, 4294967295.0}; for (size_t i = 0; i < sizeof keys / sizeof *keys; i++) { scr_arr_set_ref(a, keys[i], scr_str_retain(value)); size_t before = value->rc; check(scr_arr_peek_ref(a, keys[i]) == value, "borrowed ref lookup preserves identity"); check(value->rc == before, "borrowed ref lookup does not retain"); scr_arr_set_undefined(a, keys[i]); check(scr_arr_peek_ref(a, keys[i]) == NULL, "borrowed ref lookup of undefined"); scr_arr_delete(a, keys[i]); check(scr_arr_peek_ref(a, keys[i]) == NULL, "borrowed ref lookup of deleted slot"); } check(scr_arr_peek_ref(a, 3) == NULL, "borrowed ref lookup of hole"); scr_arr_set_len(a, 0); check(scr_arr_peek_ref(a, 0) == NULL, "borrowed ref lookup after truncation"); check(value->rc == 1, "borrowed reads leave only the original owner"); scr_str_release(value); scr_arr_release(a); } static void test_bulk_reference_ownership(void) { long strings0 = scr_str_live_count(), arrays0 = scr_arr_live_count(); ScrArr *a = scr_arr_new(SCR_ELEM_STR, 0); uint64_t slots[3]; for (size_t i = 0; i < 3; i++) { char text[] = {(char)('a' + i)}; slots[i] = (uint64_t)(uintptr_t)scr_str_from_utf8_lossy((const uint8_t *)text, 1); } check_f64(scr_arr_push_many(a, 3, slots), 3, "batch append takes all owned slots"); for (size_t i = 0; i < 3; i++) slots[i] = (uint64_t)(uintptr_t)scr_arr_get_ref(a, i); check_f64(scr_arr_unshift_many(a, 3, slots), 6, "batch prepend preserves retained aliases"); ScrArr *removed = scr_arr_splice_insert(a, 1, 2, a); check_f64(scr_arr_len(a), 10, "self insertion snapshots the source before mutation"); check_f64(scr_arr_len(removed), 2, "self insertion transfers removed ownership"); scr_arr_release(removed); ScrStr *value = scr_arr_get_ref(a, 0); ScrArr *same = scr_arr_fill_ref(a, value, 1, INFINITY); check(same == a, "reference fill returns a retained receiver"); scr_arr_release(same); scr_str_release(value); scr_arr_release(scr_arr_copy_within(a, 1, 0, INFINITY)); scr_arr_release(scr_arr_copy_within(a, 0, 1, INFINITY)); check(scr_str_live_count() == strings0 + 1, "overlapping copy retains exactly the remaining value"); scr_arr_release(scr_arr_fill_undefined(a, -4, INFINITY)); check(scr_arr_state(a, 6) == SCR_ARR_UNDEFINED, "reference fill publishes explicit undefined"); scr_arr_release(a); check(scr_str_live_count() == strings0, "bulk reference operations release every element"); check(scr_arr_live_count() == arrays0, "bulk reference operations release every array"); } /* Statement-position splices release removed values and keep inserted * ownership, on the dense path and the sparse fallback alike. */ static void test_splice_drop(void) { long strings0 = scr_str_live_count(), arrays0 = scr_arr_live_count(); ScrArr *a = scr_arr_new(SCR_ELEM_STR, 0); uint64_t slots[12]; for (size_t i = 0; i < 12; i++) { char text[] = {(char)('a' + i)}; slots[i] = (uint64_t)(uintptr_t)scr_str_from_utf8_lossy((const uint8_t *)text, 1); } scr_arr_push_many(a, 10, slots); scr_arr_set_undefined(a, 10); scr_arr_set_len(a, 13); /* holes at 11 and 12 */ scr_arr_splice_drop(a, 1, 9, 1, slots + 10); /* removes b..j (> 8 values) */ check_f64(scr_arr_len(a), 5, "drop splice removes and inserts in place"); check(scr_arr_state(a, 2) == SCR_ARR_UNDEFINED, "drop splice shifts explicit undefined"); check(scr_arr_state(a, 3) == SCR_ARR_HOLE, "drop splice shifts holes"); check(scr_str_live_count() == strings0 + 3, "drop splice releases every removed value"); scr_arr_splice_drop(a, -2, INFINITY, 0, NULL); check_f64(scr_arr_len(a), 3, "drop splice clamps relative starts and counts"); scr_arr_splice_drop(a, NAN, -1, 1, slots + 11); check_f64(scr_arr_len(a), 4, "drop splice treats NaN and negative counts as zero"); ScrStr *first = scr_arr_get_ref(a, 0); check(first == (ScrStr *)(uintptr_t)slots[11], "drop splice inserts at the clamped start"); scr_str_release(first); ScrArr *s = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_set_ref(s, 2000000, scr_str_retain((ScrStr *)(uintptr_t)slots[0])); uint64_t one = (uint64_t)(uintptr_t)scr_str_retain((ScrStr *)(uintptr_t)slots[0]); scr_arr_splice_drop(s, 0, 1, 1, &one); check_f64(scr_arr_len(s), 2000001, "sparse drop splice keeps sparse semantics"); scr_arr_release(s); scr_arr_release(a); check(scr_str_live_count() == strings0, "drop splices balance string ownership"); check(scr_arr_live_count() == arrays0, "drop splices free their temporary arrays"); } typedef struct { ScrStr *value; size_t position; } SortExpected; static int compare_expected(const void *left, const void *right) { const SortExpected *a = left, *b = right; int order = scr_str_cmp_u16(a->value, b->value); return order ? order : (a->position > b->position) - (a->position < b->position); } static void test_primitive_sort(void) { long strings0 = scr_str_live_count(), arrays0 = scr_arr_live_count(); /* More than 64 short runs exercises run-vector growth and odd merges. * Distinct allocations with equal contents make stability observable. */ enum { count = 2051 }; SortExpected expected[count]; ScrArr *words = scr_arr_new(SCR_ELEM_STR, 0); for (size_t i = 0; i < count; i++) { char text[32]; snprintf(text, sizeof(text), "word-%03zu", (i * 71) % 97); ScrStr *word = scr_str_new(text, strlen(text)); expected[i] = (SortExpected){word, i}; scr_arr_push_ref(words, word); } qsort(expected, count, sizeof(*expected), compare_expected); ScrArr *copy = scr_arr_sort_primitive(words, true); check(copy != words, "default toSorted creates a fresh array"); ScrArr *same = scr_arr_sort_primitive(words, false); check(same == words, "default sort retains receiver identity"); scr_arr_release(same); for (size_t i = 0; i < count; i++) { check(scr_arr_peek_ref(words, i) == expected[i].value, "stable default order preserves string identity"); check(scr_arr_peek_ref(copy, i) == expected[i].value, "copy has the same stable string order"); } scr_arr_release(words); check(scr_str_live_count() == strings0 + count, "copy owns every string after source release"); scr_arr_release(copy); ScrArr *sparse = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_set_f64(sparse, 4294967294.0, -0.0); scr_arr_set_f64(sparse, 10, 0.0); scr_arr_set_f64(sparse, 3, 2); scr_arr_set_f64(sparse, 1, 10); scr_arr_set_undefined(sparse, 1048576); scr_arr_set_f64(sparse, -1, 77); scr_arr_release(scr_arr_sort_primitive(sparse, false)); check_f64(scr_arr_len(sparse), 4294967295.0, "sort preserves sparse maximum length"); check(!signbit(scr_arr_get_f64(sparse, 0)) && signbit(scr_arr_get_f64(sparse, 1)), "equal numeric spellings retain original index order"); check_f64(scr_arr_get_f64(sparse, 2), 10, "default numbers compare string spellings"); check_f64(scr_arr_get_f64(sparse, 3), 2, "default numeric order is lexical"); check(scr_arr_state(sparse, 4) == SCR_ARR_UNDEFINED && scr_arr_state(sparse, 5) == SCR_ARR_HOLE && !scr_arr_has(sparse, 4294967294.0), "undefined precedes sparse holes"); check_f64(scr_arr_get_f64(sparse, -1), 77, "sort keeps ordinary numeric properties"); scr_arr_release(sparse); ScrArr *holes = scr_arr_new(SCR_ELEM_BOOL, 0); scr_arr_set_bool(holes, 4, true); scr_arr_set_bool(holes, 1, false); scr_arr_set_undefined(holes, 3); copy = scr_arr_sort_primitive(holes, true); check(!scr_arr_get_bool(copy, 0) && scr_arr_get_bool(copy, 1), "default boolean order"); for (size_t i = 2; i < 5; i++) check(scr_arr_state(copy, i) == SCR_ARR_UNDEFINED, "toSorted materializes holes"); check(!scr_arr_has(holes, 0), "toSorted leaves source holes intact"); scr_arr_release(copy); scr_arr_release(holes); ScrArr *empty = scr_arr_new(SCR_ELEM_STR, 0); scr_arr_release(scr_arr_sort_primitive(empty, false)); scr_arr_release(scr_arr_sort_primitive(empty, true)); scr_arr_release(empty); check(scr_str_live_count() == strings0, "primitive sorting releases all strings"); check(scr_arr_live_count() == arrays0, "primitive sorting releases all arrays"); } /* scr_arr_sort_values: closures built by hand with the callValue ABI. */ static long sort_calls = 0, sort_throw_at = -1; static double sort_by_floor(ScrClosure *self, double a, double b) { (void)self; sort_calls++; return floor(a) - floor(b); } static double sort_by_length(ScrClosure *self, void *a, void *b) { (void)self; double d = (double)((ScrStr *)a)->len - (double)((ScrStr *)b)->len; scr_str_release((ScrStr *)a); /* the callee owns its parameters */ scr_str_release((ScrStr *)b); if (++sort_calls == sort_throw_at) scr_throw_f64(42); return d; } static double sort_one_arg(ScrClosure *self, double a) { (void)self; sort_calls++; return a > 1 ? 1 : 0; } static double sort_nan(ScrClosure *self) { (void)self; sort_calls++; return NAN; } static void test_comparator_sort(void) { ScrClosure floor_fn = {SIZE_MAX, (void *)sort_by_floor, 0, NULL, 0}; ScrArr *nums = scr_arr_new(SCR_ELEM_F64, 0); const double input[] = {3.1, 1.1, 2.1, 1.2, 9.1, 3.2, 0.1, 1.3, 2.2, 8.1, 7.1, 6.1, 5.1, 4.1, 3.3, 2.3, 1.4, 0.2, 9.2, 8.2, 7.2, 6.2, 5.2, 4.2}; const size_t n = sizeof(input) / sizeof(*input); for (size_t i = 0; i < n; i++) scr_arr_push_f64(nums, input[i]); scr_arr_push_f64(nums, -1); /* beyond the sorted count */ scr_arr_sort_values(nums, (double)n, &floor_fn, 2, 0, NULL); bool ordered = true; for (size_t i = 1; i < n; i++) { double a = scr_arr_get_f64(nums, (double)(i - 1)), b = scr_arr_get_f64(nums, (double)i); if (floor(a) > floor(b) || (floor(a) == floor(b) && a > b)) ordered = false; } check(ordered, "comparator sort orders keys and keeps equal keys stable"); check_f64(scr_arr_get_f64(nums, (double)n), -1, "comparator sort leaves slots past the count"); ScrClosure one_fn = {SIZE_MAX, (void *)sort_one_arg, 0, NULL, 0}; ScrClosure nan_fn = {SIZE_MAX, (void *)sort_nan, 0, NULL, 0}; ScrArr *few = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 5; i++) scr_arr_push_f64(few, 5 - i); sort_calls = 0; scr_arr_sort_values(few, 5, &nan_fn, 0, 0, NULL); check(sort_calls > 0 && scr_arr_get_f64(few, 0) == 5 && scr_arr_get_f64(few, 4) == 1, "a NaN comparator result keeps the input order"); scr_arr_sort_values(few, 5, &one_fn, 1, 0, NULL); check_f64(scr_arr_len(few), 5, "a one-parameter comparator sort keeps every element"); sort_calls = 0; scr_arr_sort_values(few, 1, &nan_fn, 0, 0, NULL); check(sort_calls == 0, "a single value needs no comparator call"); scr_arr_release(few); scr_arr_release(nums); long strings0 = scr_str_live_count(); ScrClosure length_fn = {SIZE_MAX, (void *)sort_by_length, 0, NULL, 0}; ScrArr *words = scr_arr_new(SCR_ELEM_STR, 0); const char *spellings[] = {"ccc", "a", "bb", "dddd", "e", "ff", "ggg", "h", "iiiii", "jj", "k", "llll", "mmm", "n", "oo", "p", "qqq", "r", "ss", "t"}; const size_t word_count = sizeof(spellings) / sizeof(*spellings); ScrStr *originals[sizeof(spellings) / sizeof(*spellings)]; for (size_t i = 0; i < word_count; i++) { originals[i] = scr_str_new(spellings[i], strlen(spellings[i])); scr_arr_push_ref(words, scr_str_retain(originals[i])); } sort_calls = 0; sort_throw_at = 7; scr_arr_sort_values(words, (double)word_count, &length_fn, 2, 0, NULL); check(scr_exc_pending(), "a throwing comparator leaves the exception pending"); scr_exc_clear(); check(sort_calls == 7, "the sort stops at the first comparator exception"); bool unchanged = true; for (size_t i = 0; i < word_count; i++) if (scr_arr_peek_ref(words, (double)i) != originals[i]) unchanged = false; check(unchanged, "a comparator exception leaves the snapshot unchanged"); sort_throw_at = -1; scr_arr_sort_values(words, (double)word_count, &length_fn, 2, 0, NULL); check(!scr_exc_pending(), "a successful comparator sort leaves no exception"); bool stable = true; size_t last_index = 0, last_len = 0; for (size_t i = 0; i < word_count; i++) { ScrStr *s = scr_arr_peek_ref(words, (double)i); size_t index = 0; while (index < word_count && originals[index] != s) index++; size_t len = s->len; if (index == word_count || len < last_len || (i && len == last_len && index < last_index)) stable = false; last_len = len; last_index = index; } check(stable, "reference sort is a stable permutation of the original strings"); for (size_t i = 0; i < word_count; i++) scr_str_release(originals[i]); check(scr_str_live_count() == strings0 + (long)word_count, "comparator calls balance every retained argument"); scr_arr_release(words); check(scr_str_live_count() == strings0, "comparator sort releases all strings"); /* Past the dense limit the tail lives in sparse slots. */ const size_t big = ((size_t)1 << 20) + 300; ScrArr *wide = scr_arr_new(SCR_ELEM_F64, 0); for (size_t i = 0; i < big; i++) scr_arr_push_f64(wide, (double)(big - i) + 0.5); scr_arr_sort_values(wide, (double)big, &floor_fn, 2, 0, NULL); bool wide_ordered = true; for (size_t i = 0; i < big; i++) if (scr_arr_get_f64(wide, (double)i) != (double)(i + 1) + 0.5) wide_ordered = false; check(wide_ordered, "comparator sort writes sparse tail slots"); scr_arr_release(wide); } /* scr_arr_sort_dense: in-place comparator sort of dense arrays. */ static ScrArr *dense_target = NULL; static long dense_edit_at = -1; static ScrStr *dense_replacement = NULL; static double dense_by_length(ScrClosure *self, void *a, void *b) { (void)self; double d = (double)((ScrStr *)a)->len - (double)((ScrStr *)b)->len; scr_str_release((ScrStr *)a); scr_str_release((ScrStr *)b); if (++sort_calls == dense_edit_at) { /* Overwrite one element, then truncate: the dropped originals must * survive in the sort's own buffer. */ scr_arr_set_ref(dense_target, 0, scr_str_retain(dense_replacement)); scr_arr_set_len(dense_target, 0); } if (sort_calls == sort_throw_at) scr_throw_f64(7); return d; } static double dense_observe(ScrClosure *self, double a, double b) { (void)self; /* The receiver keeps its unsorted values while the comparator runs. */ if (sort_calls++ == 0 && scr_arr_get_f64(dense_target, 0) != 9) sort_throw_at = -2; return a - b; } static void test_dense_comparator_sort(void) { ScrClosure floor_fn = {SIZE_MAX, (void *)sort_by_floor, 0, NULL, 0}; ScrClosure observe_fn = {SIZE_MAX, (void *)dense_observe, 0, NULL, 0}; ScrArr *nums = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 100; i++) scr_arr_push_f64(nums, (double)((i * 37) % 100) + (i % 2) * 0.5); scr_arr_push_f64(nums, 9); /* keys stay distinct by floor */ scr_arr_set_f64(nums, 0, 9); dense_target = nums; sort_calls = 0; sort_throw_at = -1; check(scr_arr_sort_dense(nums, &observe_fn, 2, false, 0, NULL), "a dense number array sorts in place"); check(sort_throw_at == -1, "the comparator sees the unsorted receiver"); bool ordered = true; for (size_t i = 1; i < 101; i++) if (scr_arr_get_f64(nums, (double)(i - 1)) > scr_arr_get_f64(nums, (double)i)) ordered = false; check(ordered && scr_arr_len(nums) == 101, "dense number sort orders every value"); ScrArr *holes = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(holes, 3); scr_arr_set_f64(holes, 2, 1); sort_calls = 0; check(!scr_arr_sort_dense(holes, &floor_fn, 2, false, 0, NULL) && sort_calls == 0, "an array with holes takes the snapshot path without comparator calls"); ScrArr *undef = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(undef, 3); scr_arr_set_undefined(undef, 1); scr_arr_push_f64(undef, 1); check(!scr_arr_sort_dense(undef, &floor_fn, 2, true, 0, NULL) && sort_calls == 0, "storage-level undefined takes the snapshot path"); ScrArr *one = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(one, 5); check(scr_arr_sort_dense(one, &floor_fn, 2, false, 0, NULL) && sort_calls == 0, "a single value needs no comparator call"); size_t rc = one->rc; one->rc = SIZE_MAX; /* as publication marks it */ check(!scr_arr_sort_dense(one, &floor_fn, 2, false, 0, NULL), "a published array takes the general path"); one->rc = rc; scr_arr_release(one); scr_arr_release(undef); scr_arr_release(holes); scr_arr_release(nums); long strings0 = scr_str_live_count(); ScrClosure length_fn = {SIZE_MAX, (void *)dense_by_length, 0, NULL, 0}; const char *spellings[] = {"ccc", "a", "bb", "dddd", "e", "ff", "ggg", "h", "iiiii", "jj", "k", "llll", "mmm", "n", "oo", "p", "qqq", "r", "ss", "t"}; const size_t count = sizeof(spellings) / sizeof(*spellings); for (int owned = 0; owned < 2; owned++) { for (long at = 1; at <= 40; at += 39) { ScrArr *words = scr_arr_new(SCR_ELEM_STR, 0); for (size_t i = 0; i < count; i++) scr_arr_push_ref(words, scr_str_new(spellings[i], strlen(spellings[i]))); ScrStr *first = scr_arr_peek_ref(words, 0); sort_calls = 0; sort_throw_at = at; dense_edit_at = -1; check(scr_arr_sort_dense(words, &length_fn, 2, owned, 0, NULL) && scr_exc_pending(), "a throwing comparator leaves the exception pending"); scr_exc_clear(); check(sort_calls == at && scr_arr_len(words) == count, "the dense sort stops at the first comparator exception"); if (!owned) { check(scr_arr_peek_ref(words, 0) == first, "an exception leaves the receiver unwritten"); } else { /* A private copy is discarded after an exception; it must still * own every element exactly once. */ bool permutation = true; for (size_t i = 0; i < count; i++) { size_t uses = 0; for (size_t j = 0; j < count; j++) uses += strcmp(((ScrStr *)scr_arr_peek_ref(words, (double)j))->data, spellings[i]) == 0; permutation &= uses == 1; } check(permutation, "an exception leaves the private copy a permutation"); } scr_arr_release(words); } } check(scr_str_live_count() == strings0, "throwing dense sorts release every string"); dense_replacement = scr_str_new("zz", 2); ScrArr *words = scr_arr_new(SCR_ELEM_STR, 0); for (size_t i = 0; i < count; i++) scr_arr_push_ref(words, scr_str_new(spellings[i], strlen(spellings[i]))); dense_target = words; sort_calls = 0; sort_throw_at = -1; dense_edit_at = 3; check(scr_arr_sort_dense(words, &length_fn, 2, false, 0, NULL) && !scr_exc_pending(), "a comparator that truncates the receiver still completes"); dense_edit_at = -1; bool lengths = scr_arr_len(words) == count; for (size_t i = 1; lengths && i < count; i++) lengths = ((ScrStr *)scr_arr_peek_ref(words, (double)(i - 1)))->len <= ((ScrStr *)scr_arr_peek_ref(words, (double)i))->len; check(lengths, "the sorted original values are written back after truncation"); scr_arr_release(words); scr_str_release(dense_replacement); check(scr_str_live_count() == strings0, "a mutating comparator leaks no string"); dense_target = NULL; } static void test_exact_construction(void) { /* Construction reserves the requested slots (rounded up to the small * allocator's block where it is live); appends past them grow * geometrically, and every new slot starts as a hole. */ ScrArr *one = scr_arr_new(SCR_ELEM_F64, 1); check(one->cap >= 1 && one->cap < 4 && scr_arr_len(one) == 0 && !scr_arr_has(one, 0), "one-slot construction keeps an exactly sized empty dense prefix"); scr_arr_push_f64(one, 1); size_t first_cap = one->cap; for (int i = 2; i <= 6; i++) scr_arr_push_f64(one, i); check(one->cap >= 6 && one->cap > first_cap && scr_arr_len(one) == 6, "appends past an exact construction grow the dense prefix"); for (int i = 0; i < 6; i++) check(scr_arr_get_number(one, i) == i + 1, "grown values kept"); scr_arr_set_len(one, (double)one->cap + 3); check(!scr_arr_has(one, 6) && !scr_arr_has(one, (double)one->cap - 1), "spare capacity after growth from an exact size reads as holes"); scr_arr_release(one); #if SCR_SMALL_ALLOC /* Requests round to 16 bytes before the size class: two slots (18 * bytes) get a 32-byte block, so one append needs no reallocation. */ ScrArr *two = scr_arr_new(SCR_ELEM_F64, 2); scr_arr_push_f64(two, 1); scr_arr_push_f64(two, 2); uint64_t *before = two->data; scr_arr_push_f64(two, 3); check(two->cap == 3 && two->data == before && scr_arr_get_number(two, 2) == 3, "an exact two-slot construction has room for one append"); scr_arr_release(two); #endif ScrArr *refs = scr_arr_new(SCR_ELEM_STR, 3); scr_arr_push_ref(refs, scr_str_new("a", 1)); scr_arr_set_ref(refs, 4, scr_str_new("e", 1)); check(scr_arr_len(refs) == 5 && scr_arr_has(refs, 0) && !scr_arr_has(refs, 1) && !scr_arr_has(refs, 3) && scr_arr_has(refs, 4), "index write past an exact construction leaves holes between"); scr_arr_release(refs); } /* The hole-free prefix bound never covers a non-value state, and the * operations that keep an array hole-free keep the bound at its length. */ static bool filled_ok(const ScrArr *a) { if (a->filled > a->len || a->filled > a->cap) return false; for (size_t i = 0; i < a->filled; i++) if (a->present[i] != SCR_ARR_VALUE) return false; return true; } static void test_filled_prefix(void) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 8; i++) scr_arr_push_f64(a, i); check(a->filled == 8 && filled_ok(a), "pushes keep the bound at the length"); scr_arr_set_len(a, 12); check(a->filled == 8 && filled_ok(a), "length growth leaves the bound below the holes"); scr_arr_set_f64(a, 8, 8); check(a->filled == 9 && filled_ok(a), "a write at the bound extends it"); scr_arr_set_f64(a, 11, 11); check(a->filled == 9 && filled_ok(a), "a write past a hole does not extend it"); scr_arr_release(scr_arr_fill_f64(a, 0, 0, INFINITY)); check(a->filled == 12 && filled_ok(a), "fill over the whole array restores it"); scr_arr_delete(a, 3); check(a->filled == 3 && filled_ok(a), "delete cuts the bound at the hole"); scr_arr_set_f64(a, 3, 3); check(a->filled == 4 && filled_ok(a), "refilling the hole continues the prefix"); scr_arr_set_undefined(a, 1); check(a->filled == 1 && filled_ok(a), "present undefined cuts the bound"); scr_arr_set_len(a, 0); check(a->filled == 0 && filled_ok(a), "truncation cuts the bound"); scr_arr_release(a); ScrArr *b = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 10; i++) scr_arr_push_f64(b, i); check(scr_arr_shift_f64(b) == 0 && b->filled == 9 && filled_ok(b), "shift keeps a full bound"); scr_arr_unshift_f64(b, -1); check(b->filled == 10 && filled_ok(b), "unshift keeps a full bound"); scr_arr_release(scr_arr_splice(b, 2, 3)); check(b->filled == 7 && filled_ok(b), "splice removal keeps a full bound"); double values[2] = {5, 6}; uint64_t two[2]; memcpy(two, values, sizeof two); scr_arr_splice_drop(b, 1, 0, 2, two); check(b->filled == 9 && filled_ok(b), "splice insertion keeps a full bound"); scr_arr_release(scr_arr_reverse(b)); check(b->filled == 9 && filled_ok(b), "reverse keeps a full bound"); scr_arr_release(scr_arr_copy_within(b, 0, 4, 6)); check(b->filled == 9 && filled_ok(b), "copyWithin inside the prefix keeps it"); scr_arr_set_len(b, 12); scr_arr_release(scr_arr_copy_within(b, 2, 10, 12)); check(b->filled == 2 && filled_ok(b), "copyWithin of holes cuts the bound"); ScrArr *s = scr_arr_slice(b, 0, 2); check(s->filled == 2 && filled_ok(s), "slice of a hole-free range is hole-free"); scr_arr_release(s); s = scr_arr_slice(b, 0, INFINITY); check(s->filled == 2 && filled_ok(s), "slice keeps holes out of the bound"); scr_arr_release(s); /* The rescan after a copy stops exactly at the first non-value state, * whichever eight-state step holds it, and a slice that starts past a * source's bound still finds the values after it. */ for (size_t hole = 0; hole < 37; hole++) { ScrArr *w = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 37; i++) scr_arr_push_f64(w, i); if (hole % 2) scr_arr_set_undefined(w, (double)hole); else scr_arr_delete(w, (double)hole); for (size_t from = 0; from < 20; from += 3) { ScrArr *c = scr_arr_slice(w, (double)from, INFINITY); size_t want = hole >= from ? hole - from : 37 - from; check(c->filled == want && filled_ok(c), "slice bound ends at the first non-value"); scr_arr_release(c); } ScrArr *c = scr_arr_slice(w, 30, 10); check(c->len == 0 && c->filled == 0, "an empty slice has an empty bound"); scr_arr_release(c); scr_arr_release(w); } scr_arr_release(scr_arr_reverse(b)); check(filled_ok(b), "reverse of a holey array recomputes the bound"); scr_arr_release(scr_arr_sort_primitive(b, false)); check(filled_ok(b), "sorting moves holes past the bound"); for (size_t i = 0; i < b->len; i++) { if (i < b->cap && b->present[i] != SCR_ARR_VALUE) break; check(i < b->filled, "sorted values form the bound"); } while (scr_arr_len(b) > 0) { scr_arr_pop_state(b, NULL); check(filled_ok(b), "pop keeps the bound below the length"); } scr_arr_release(b); ScrArr *r = scr_arr_new(SCR_ELEM_STR, 0); for (int i = 0; i < 4; i++) scr_arr_push_ref(r, scr_str_new("ab", 2)); check(r->filled == 4 && filled_ok(r), "reference pushes keep the bound"); scr_arr_delete(r, 0); check(r->filled == 0 && filled_ok(r), "deleting the first reference empties the bound"); scr_arr_set_ref(r, 0, scr_str_new("c", 1)); check(r->filled == 1 && filled_ok(r), "a write at an empty bound extends it by one"); scr_arr_release(r); } /* Boolean arrays store one byte per dense element, followed by the states in * the same allocation. Growth moves the states past the larger value area, * densifying absorbs sparse entries into bytes, and every moving and copying * operation keeps values, holes and present undefined in place. */ static bool bool_layout_ok(const ScrArr *a) { return a->cap == 0 || a->present == (const uint8_t *)a->data + a->cap; } static bool bool_states(const ScrArr *a, const char *want) { if (scr_arr_len(a) != (double)strlen(want)) return false; for (size_t i = 0; want[i]; i++) { double state = scr_arr_state(a, (double)i); char got = state == SCR_ARR_HOLE ? '_' : state == SCR_ARR_UNDEFINED ? 'u' : scr_arr_get_bool((ScrArr *)a, (double)i) ? 't' : 'f'; if (got != want[i]) return false; } return true; } static void test_bool_bytes(void) { ScrArr *a = scr_arr_new(SCR_ELEM_BOOL, 0); for (int i = 0; i < 1000; i++) scr_arr_push_bool(a, i % 3 == 0); check(bool_layout_ok(a) && a->filled == 1000, "bool pushes grow one-byte storage"); bool values_ok = true; for (int i = 0; i < 1000; i++) { values_ok &= ((const uint8_t *)a->data)[i] == (i % 3 == 0); values_ok &= scr_arr_get_bool(a, i) == (i % 3 == 0); } check(values_ok, "bool values are stored as 0/1 bytes"); check(scr_arr_index_of_bool(a, false) == 1 && scr_arr_includes_bool(a, true), "bool search reads bytes"); scr_arr_set_len(a, 5); scr_arr_set_len(a, 8); scr_arr_set_undefined(a, 6); check(bool_layout_ok(a) && bool_states(a, "tfftf_u_"), "bool truncation clears bytes"); ScrArr *s = scr_arr_new(SCR_ELEM_BOOL, 0); scr_arr_set_bool(s, 0, true); scr_arr_set_bool(s, 3000000, true); scr_arr_set_bool(s, 2000000, false); check(arr_sparse_len(s) == 2 && !scr_arr_get_bool(s, 2000000) && scr_arr_get_bool(s, 3000000), "bool sparse entries keep their values"); scr_arr_set_len(s, 2000001); scr_arr_release(scr_arr_fill_bool(s, true, 1, 2000000)); check(arr_sparse_len(s) == 0 && bool_layout_ok(s) && !scr_arr_get_bool(s, 2000000) && scr_arr_get_bool(s, 1999999) && scr_arr_index_of_bool(s, false) == 2000000, "densifying absorbs sparse bool entries into bytes"); scr_arr_release(s); ScrArr *m = scr_arr_new(SCR_ELEM_BOOL, 0); uint64_t slots[3] = {1, 0, 1}; scr_arr_push_many(m, 3, slots); scr_arr_unshift_many(m, 2, slots + 1); check(bool_states(m, "fttft"), "slot buffers narrow to bytes"); scr_arr_unshift_bool(m, false); scr_arr_set_undefined(m, 2); scr_arr_delete(m, 4); check(bool_states(m, "ffut_t"), "unshift moves bytes and states together"); ScrArr *removed = scr_arr_splice(m, 1, 2); check(bool_states(removed, "fu") && bool_states(m, "ft_t"), "splice moves bytes out"); scr_arr_release(removed); ScrArr *items = scr_arr_new(SCR_ELEM_BOOL, 0); scr_arr_push_bool(items, true); scr_arr_push_bool(items, false); scr_arr_release(scr_arr_splice_insert(m, 1, 1, items)); check(bool_states(m, "ftf_t"), "splice insertion copies bytes in"); scr_arr_splice_drop(m, 0, 1, 2, slots); check(bool_states(m, "tftf_t"), "statement splice stores bytes"); scr_arr_release(scr_arr_reverse(m)); check(bool_states(m, "t_ftft"), "reverse swaps bytes"); ScrArr *copy = scr_arr_slice(m, 1, INFINITY); check(bool_states(copy, "_ftft") && bool_layout_ok(copy), "slice copies bytes"); scr_arr_release(copy); copy = scr_arr_to_reversed(m); check(bool_states(copy, "tftfut"), "toReversed copies bytes and materializes holes"); scr_arr_release(copy); scr_arr_concat_copy(m, items); scr_arr_push_spread(m, m); check(bool_states(m, "t_ftfttftuftfttf"), "concat keeps holes; self spread reads them as undefined"); scr_arr_release(scr_arr_copy_within(m, 0, 2, 5)); check(scr_arr_index_eq(m, 0, items, 1) && scr_arr_index_eq(m, 1, items, 0), "copyWithin moves bytes and index equality reads them"); check(scr_arr_shift_bool(m) == false && scr_arr_pop_bool(m) == false, "shift and pop read bytes"); scr_arr_release(scr_arr_sort_primitive(m, false)); bool sorted = true; for (double i = 1; i < scr_arr_len(m); i++) if (scr_arr_state(m, i) == SCR_ARR_VALUE && scr_arr_state(m, i - 1) == SCR_ARR_VALUE) sorted &= scr_arr_get_bool(m, i - 1) <= scr_arr_get_bool(m, i); check(sorted, "default sort orders bytes"); ScrStr *sep = scr_str_new(",", 1); ScrStr *joined = scr_arr_join(items, sep); check(strcmp(joined->data, "true,false") == 0, "join reads bytes"); scr_str_release(joined); scr_str_release(sep); scr_arr_release(items); scr_arr_release(m); scr_arr_release(a); } /* Small constructions keep their first slots in the array's own block, * right after the header; growth moves them to an ordinary * allocation, and storage replacement, outlining and release never free * the inline room on its own (ASan reports an invalid free otherwise). */ static bool inline_slots(const ScrArr *a) { return a->data == (const uint64_t *)((const char *)a + ((sizeof(ScrArr) + 7) & ~(size_t)7)); } static void test_inline_slots(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); long arrays0 = scr_arr_live_count(); #endif ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 4); check(!inline_slots(plain), "plain constructions keep separate storage"); scr_arr_release(plain); ScrArr *a = scr_arr_new_small(SCR_ELEM_F64); check(inline_slots(a) && a->cap >= 4 && a->len == 0 && a->present == (uint8_t *)(a->data + a->cap), "small construction places slots and states in the array block"); size_t inline_cap = a->cap; for (size_t i = 0; i < inline_cap; i++) scr_arr_push_f64(a, (double)i); check(inline_slots(a) && a->filled == inline_cap, "appends within the inline capacity stay inline"); scr_arr_push_f64(a, 100); check(!inline_slots(a) && a->cap > inline_cap && scr_arr_get_f64(a, 0) == 0 && scr_arr_get_f64(a, (double)inline_cap - 1) == (double)inline_cap - 1 && scr_arr_get_f64(a, (double)inline_cap) == 100 && a->present == (uint8_t *)(a->data + a->cap), "growth moves inline slots to their own allocation"); scr_arr_release(a); ScrArr *flags = scr_arr_new_small(SCR_ELEM_BOOL); check(inline_slots(flags) && flags->present == (uint8_t *)flags->data + flags->cap, "boolean inline slots are one byte each"); scr_arr_push_bool(flags, true); scr_arr_set_bool(flags, 3, true); check(scr_arr_get_bool(flags, 0) && !scr_arr_has(flags, 1) && scr_arr_get_bool(flags, 3), "boolean inline slots keep holes"); scr_arr_release(flags); ScrArr *words = scr_arr_new_small(SCR_ELEM_STR); scr_arr_push_ref(words, scr_str_new("a", 1)); scr_arr_push_ref(words, scr_str_new("b", 1)); scr_arr_set_ref(words, 3000000, scr_str_new("far", 3)); check(inline_slots(words) && arr_sparse_len(words) == 1, "a far index leaves inline slots in place"); scr_arr_unshift_ref(words, scr_str_new("z", 1)); check(!inline_slots(words) && scr_arr_len(words) == 3000002 && strcmp(((ScrStr *)scr_arr_peek_ref(words, 0))->data, "z") == 0 && strcmp(((ScrStr *)scr_arr_peek_ref(words, 2))->data, "b") == 0 && strcmp(((ScrStr *)scr_arr_peek_ref(words, 3000001))->data, "far") == 0, "rebuilding storage of an inline array keeps every element"); scr_arr_release(words); ScrArr *moved = scr_arr_new_small(SCR_ELEM_STR); ScrArr *copy = scr_arr_new_small(SCR_ELEM_STR); scr_arr_push_ref(moved, scr_str_new("x", 1)); scr_arr_push_ref(moved, scr_str_new("y", 1)); scr_arr_concat_copy(copy, moved); check(inline_slots(copy) && scr_arr_len(copy) == 2, "a copy into small storage stays inline"); scr_arr_outline(moved); check(!inline_slots(moved) && strcmp(((ScrStr *)scr_arr_peek_ref(moved, 1))->data, "y") == 0, "outlining moves the slots and keeps the elements"); scr_arr_outline(moved); scr_arr_release(scr_arr_splice(copy, 0, 1)); scr_arr_release(scr_arr_reverse(copy)); check(scr_arr_len(copy) == 1 && strcmp(((ScrStr *)scr_arr_peek_ref(copy, 0))->data, "y") == 0, "moving operations work on inline slots"); scr_arr_release(copy); scr_arr_release(moved); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0 && scr_arr_live_count() == arrays0, "inline-slot arrays release every element and header"); #endif } /* Keyed sorts for recognized numeric comparators: the same stable result as * the comparator sort, without comparator calls unless a key is NaN or -0. */ static double keyed_ascending(ScrClosure *self, double a, double b) { (void)self; sort_calls++; return a - b; } static double keyed_descending(ScrClosure *self, double a, double b) { (void)self; sort_calls++; return b - a; } static double keyed_rec_ascending(ScrClosure *self, void *a, void *b) { (void)self; sort_calls++; double d = (double)((MockRec *)a)->value - (double)((MockRec *)b)->value; mock_release(a); mock_release(b); return d; } static double keyed_rec_descending(ScrClosure *self, void *a, void *b) { (void)self; sort_calls++; double d = (double)((MockRec *)b)->value - (double)((MockRec *)a)->value; mock_release(a); mock_release(b); return d; } /* Reads value 0 as -0, which the comparator also treats as equal to 0. A * first scan reads keys without storing them, so a key may be read twice, * and placing a short tail reads head keys again in binary searches. */ static long key_reads = 0; static double keyed_rec_key(ScrClosure *self, void *a) { (void)self; key_reads++; int value = ((MockRec *)a)->value; mock_release(a); return value == 0 ? -0.0 : (double)value; } static uint32_t keyed_seed = 7; static uint32_t keyed_random(void) { keyed_seed = keyed_seed * 1103515245u + 12345u; return keyed_seed >> 8; } static void keyed_fill(ScrArr *a, ScrArr *b, size_t n, int shape) { for (size_t i = 0; i < n; i++) { double v; if (shape == 0) v = (double)(keyed_random() % 64) - 32; /* small integers with ties */ else if (shape == 1) v = ((double)keyed_random() - 4e6) / 977.0 * 1e10; /* wide doubles */ else if (shape == 2) v = (double)i; /* presorted */ else if (shape == 3) v = (double)(n - i) * 0.25; /* strictly reversed */ else if (shape == 4) v = (double)((i % (n / 3 + 1)) / 2); /* three runs with ties */ else if (shape == 5) v = i + 3 < n ? (double)i : (double)(keyed_random() % 64); /* sorted, then appended */ else v = i < 3 ? (double)(keyed_random() % 64) : (double)(i / 2); /* a few, then sorted */ if (shape == 1 && i == 3) v = INFINITY; if (shape == 1 && i == 5) v = -INFINITY; scr_arr_push_f64(a, v); scr_arr_push_f64(b, v); } } static bool keyed_same_numbers(ScrArr *a, ScrArr *b) { if (scr_arr_len(a) != scr_arr_len(b)) return false; for (size_t i = 0; i < (size_t)scr_arr_len(a); i++) { double x = scr_arr_get_f64(a, (double)i), y = scr_arr_get_f64(b, (double)i); if (memcmp(&x, &y, sizeof x) != 0) return false; } return true; } static void test_keyed_sort(void) { ScrClosure ascending = {SIZE_MAX, (void *)keyed_ascending, 0, NULL, 0}; ScrClosure descending = {SIZE_MAX, (void *)keyed_descending, 0, NULL, 0}; const size_t sizes[] = {2, 5, 40, 49, 1000, 70000, 140000}; /* the last takes 11-bit digits */ const size_t size_count = sizeof(sizes) / sizeof(*sizes); bool same = true, uncalled = true; for (size_t s = 0; s < size_count; s++) { for (int shape = 0; shape < 7; shape++) { for (int order = -1; order <= 1; order += 2) { ScrClosure *cmp = order > 0 ? &ascending : &descending; for (int path = 0; path < 3; path++) { ScrArr *expected = scr_arr_new(SCR_ELEM_F64, 0); ScrArr *actual = scr_arr_new(SCR_ELEM_F64, 0); keyed_fill(expected, actual, sizes[s], shape); scr_arr_sort_values(expected, (double)sizes[s], cmp, 2, 0, NULL); sort_calls = 0; if (path == 0) scr_arr_sort_values(actual, (double)sizes[s], cmp, 2, order, NULL); else scr_arr_sort_dense(actual, cmp, 2, path == 2, order, NULL); uncalled &= sort_calls == 0; same &= keyed_same_numbers(expected, actual); scr_arr_release(expected); scr_arr_release(actual); } } } } check(same, "keyed number sorts match the comparator sort"); check(uncalled, "keyed number sorts call no comparator"); const double specials[] = {NAN, -0.0}; for (size_t k = 0; k < 2; k++) { ScrArr *expected = scr_arr_new(SCR_ELEM_F64, 0); ScrArr *actual = scr_arr_new(SCR_ELEM_F64, 0); keyed_fill(expected, actual, 300, 0); scr_arr_set_f64(expected, 150, specials[k]); scr_arr_set_f64(actual, 150, specials[k]); scr_arr_sort_values(expected, 300, &ascending, 2, 0, NULL); sort_calls = 0; scr_arr_sort_dense(actual, &ascending, 2, true, 1, NULL); check(sort_calls > 0 && keyed_same_numbers(expected, actual), k == 0 ? "a NaN key sorts through the comparator" : "a -0 key sorts through the comparator"); scr_arr_release(expected); scr_arr_release(actual); } ScrClosure rec_ascending = {SIZE_MAX, (void *)keyed_rec_ascending, 0, NULL, 0}; ScrClosure rec_descending = {SIZE_MAX, (void *)keyed_rec_descending, 0, NULL, 0}; ScrClosure rec_key = {SIZE_MAX, (void *)keyed_rec_key, 0, NULL, 0}; long live0 = mock_live; bool identical = true, direct = true, fallback = true; for (size_t s = 0; s < size_count; s++) { for (int order = -1; order <= 1; order += 2) { ScrClosure *cmp = order > 0 ? &rec_ascending : &rec_descending; for (int variant = 0; variant < 8; variant++) { /* Random keys, three ascending runs, ascending keys with a few or an * eighth appended, each without and with a -0 key. */ bool zero = variant & 1, runs = variant / 2 == 1, appended = variant / 2 >= 2; size_t tail = variant / 2 == 3 ? sizes[s] / 8 : 4; size_t n = sizes[s]; ScrArr *expected = scr_arr_new_ops(&mock_ops, 0); ScrArr *actual = scr_arr_new_ops(&mock_ops, 0); for (size_t i = 0; i < n; i++) { int value = runs ? (int)((i % (n / 3 + 1)) * 40 / (n / 3 + 1)) + 1 : appended ? (i + tail < n ? (int)(i * 40 / n) + 1 : (int)(keyed_random() % 45)) : (int)(keyed_random() % 50) - 25; if (value == 0) value = 25; if (zero && i == n / 2) value = 0; MockRec *r = mock_new(value); scr_arr_push_ref(expected, r); scr_arr_push_ref(actual, mock_retain(r)); } scr_arr_sort_values(expected, (double)n, cmp, 2, 0, NULL); sort_calls = 0; key_reads = 0; if (s % 2) scr_arr_sort_values(actual, (double)n, cmp, 2, order, &rec_key); else scr_arr_sort_dense(actual, cmp, 2, s % 4 == 2, order, &rec_key); for (size_t i = 0; i < n; i++) identical &= scr_arr_peek_ref(expected, (double)i) == scr_arr_peek_ref(actual, (double)i); if (zero) fallback &= sort_calls > 0; else direct &= sort_calls == 0 && key_reads <= 2 * (long)n + (long)tail * 64; scr_arr_release(expected); scr_arr_release(actual); } } } check(identical, "keyed record sorts keep the comparator sort's stable permutation"); check(direct, "keyed record sorts read keys a bounded number of times and call no comparator"); check(fallback, "a -0 record key sorts through the comparator"); check(mock_live == live0, "keyed record sorts balance every key read"); } /* Typed-array sorts: the default numeric order and recognized comparator * orders against the comparator sort, which follows Node's merge schedule. */ static double typed_mod_ascending(ScrClosure *self, double a, double b) { (void)self; sort_calls++; return fmod(fabs(a), 7) - fmod(fabs(b), 7); } static double typed_default_order(ScrClosure *self, double a, double b) { (void)self; /* The default TypedArray order: numbers ascending, -0 before 0, NaN last. */ if (a != a) return b != b ? 0 : 1; if (b != b) return -1; if (a < b) return -1; if (a > b) return 1; if (a == 0 && signbit(a) != signbit(b)) return signbit(a) ? -1 : 1; return 0; } static double typed_throwing(ScrClosure *self, double a, double b) { (void)self; if (++sort_calls == sort_throw_at) scr_throw_f64(5); return a - b; } static bool typed_same(const ScrBytes *a, const ScrBytes *b) { return a->len == b->len && memcmp(a->data, b->data, a->len * scr_bytes_elem_size(a->elem)) == 0; } static double typed_value(ScrBytesElem elem, size_t i, int shape) { double r = (double)keyed_random(); if (shape == 1) return (double)i; /* presorted */ if (shape == 2) return (double)(1000 - i % 1000); /* descending runs */ switch (elem) { case SCR_BYTES_U8: case SCR_BYTES_U8C: return fmod(r, 256); case SCR_BYTES_I8: return fmod(r, 256) - 128; case SCR_BYTES_U16: return fmod(r, 65536); case SCR_BYTES_I16: return fmod(r, 65536) - 32768; case SCR_BYTES_U32: return r * 256; case SCR_BYTES_I32: return r * 256 - 2147483648.0; default: return (r - 8e6) / 3.0; } } static void test_typed_sort(void) { const ScrBytesElem elems[] = {SCR_BYTES_U8, SCR_BYTES_U8C, SCR_BYTES_I8, SCR_BYTES_U16, SCR_BYTES_I16, SCR_BYTES_U32, SCR_BYTES_I32, SCR_BYTES_F32, SCR_BYTES_F64}; const size_t sizes[] = {0, 1, 2, 7, 48, 49, 1000, 150000}; ScrClosure natural = {SIZE_MAX, (void *)typed_default_order, 0, NULL, 0}; ScrClosure ascending = {SIZE_MAX, (void *)keyed_ascending, 0, NULL, 0}; ScrClosure descending = {SIZE_MAX, (void *)keyed_descending, 0, NULL, 0}; ScrClosure by_mod = {SIZE_MAX, (void *)typed_mod_ascending, 0, NULL, 0}; bool numeric = true, keyed = true, compared = true; for (size_t e = 0; e < sizeof(elems) / sizeof(*elems); e++) { for (size_t s = 0; s < sizeof(sizes) / sizeof(*sizes); s++) { for (int shape = 0; shape < 3; shape++) { size_t n = sizes[s]; ScrBytes *base = scr_bytes_new(elems[e], (double)n); for (size_t i = 0; i < n; i++) scr_bytes_set(base, (double)i, typed_value(elems[e], i, shape)); bool floats = elems[e] == SCR_BYTES_F32 || elems[e] == SCR_BYTES_F64; if (floats && n > 6 && shape == 0) { scr_bytes_set(base, 1, NAN); scr_bytes_set(base, 3, -0.0); scr_bytes_set(base, 5, 0.0); } ScrBytes *expected = scr_bytes_copy(base), *actual = scr_bytes_copy(base); scr_bytes_sort_compare(expected, &natural, 2); numeric &= scr_bytes_sort_numeric(actual, 0) && typed_same(expected, actual); scr_bytes_release(expected); scr_bytes_release(actual); for (int order = -1; order <= 1; order += 2) { expected = scr_bytes_copy(base); actual = scr_bytes_copy(base); ScrClosure *cmp = order > 0 ? &ascending : &descending; scr_bytes_sort_compare(expected, cmp, 2); sort_calls = 0; bool handled = scr_bytes_sort_numeric(actual, order); /* NaN and -0 refuse the comparator order and leave the elements. */ bool special = floats && n > 6 && shape == 0; keyed &= sort_calls == 0 && handled == !special; keyed &= handled ? typed_same(expected, actual) : typed_same(base, actual); scr_bytes_release(expected); scr_bytes_release(actual); } if (n <= 1000) { /* The comparator sort is stable for a consistent comparator. */ actual = scr_bytes_copy(base); scr_bytes_sort_compare(actual, &by_mod, 2); for (size_t i = 1; i < n; i++) { double a = fmod(fabs(scr_bytes_get(actual, (double)(i - 1))), 7); double b = fmod(fabs(scr_bytes_get(actual, (double)i)), 7); if (a == a && b == b && a > b) compared = false; } scr_bytes_release(actual); } scr_bytes_release(base); } } } check(numeric, "typed default sorts match the default order's comparator sort"); check(keyed, "typed keyed sorts match their comparators and refuse NaN and -0"); check(compared, "typed comparator sorts order by the comparator"); /* Node's merge schedule: three elements compare (b, c) first, then a. */ ScrBytes *three = scr_bytes_new(SCR_BYTES_F64, 3); scr_bytes_set(three, 0, 3); scr_bytes_set(three, 1, 1); scr_bytes_set(three, 2, 2); ScrClosure throwing = {SIZE_MAX, (void *)typed_throwing, 0, NULL, 0}; sort_calls = 0; sort_throw_at = 2; scr_bytes_sort_compare(three, &throwing, 2); check(scr_exc_pending(), "a throwing typed comparator leaves the exception pending"); scr_exc_clear(); check(sort_calls == 2 && scr_bytes_get(three, 0) == 3 && scr_bytes_get(three, 2) == 2, "a typed comparator exception leaves the elements unwritten"); sort_throw_at = -1; scr_bytes_sort_compare(three, &throwing, 2); check(!scr_exc_pending() && scr_bytes_get(three, 0) == 1 && scr_bytes_get(three, 2) == 3, "a typed comparator sort writes the sorted values back"); scr_bytes_release(three); } /* scr_arr_dense_slots: the borrowed slots of hole-free dense arrays. */ static void test_dense_slots(void) { size_t len = 0; ScrArr *nums = scr_arr_new(SCR_ELEM_F64, 0); check(scr_arr_dense_slots(nums, &len) == NULL, "an empty array has no dense slots"); for (int i = 0; i < 5; i++) scr_arr_push_f64(nums, i * 1.5); const uint64_t *slots = scr_arr_dense_slots(nums, &len); bool values = slots != NULL && len == 5; for (size_t i = 0; values && i < len; i++) { double v; memcpy(&v, &slots[i], sizeof v); values = v == (double)i * 1.5; } check(values, "dense number slots hold the values in order"); scr_arr_set_undefined(nums, 2); check(scr_arr_dense_slots(nums, &len) == NULL, "storage-level undefined has no dense slots"); ScrArr *holes = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(holes, 1); scr_arr_set_f64(holes, 3, 2); check(scr_arr_dense_slots(holes, &len) == NULL, "holes have no dense slots"); ScrArr *flags = scr_arr_new(SCR_ELEM_BOOL, 0); scr_arr_push_bool(flags, true); check(scr_arr_dense_slots(flags, &len) == NULL, "boolean arrays have no 8-byte slots"); ScrArr *words = scr_arr_new(SCR_ELEM_STR, 0); ScrStr *word = scr_str_new("w", 1); scr_arr_push_ref(words, scr_str_retain(word)); slots = scr_arr_dense_slots(words, &len); check(slots != NULL && len == 1 && (void *)(uintptr_t)slots[0] == word, "dense reference slots hold borrowed pointers"); ScrArr *wide = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(wide, 1); scr_arr_set_f64(wide, 3000000, 2); check(scr_arr_dense_slots(wide, &len) == NULL, "a sparse tail has no dense slots"); scr_str_release(word); scr_arr_release(wide); scr_arr_release(words); scr_arr_release(flags); scr_arr_release(holes); scr_arr_release(nums); } /* shift and splice(0, n) advance dense storage into head room instead of * moving the tail; unshift reuses that room, growth gives it back, and a * drained array starts over at the front of its allocation. */ static void test_head_room(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); long arrays0 = scr_arr_live_count(); #endif ScrArr *q = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 64; i++) scr_arr_push_f64(q, i); uint64_t *block = q->data; size_t cap = q->cap; check_f64(scr_arr_shift_f64(q), 0, "shift answers the first element"); check(q->data == block + 1 && q->cap == cap - 1 && q->len == 63, "shift advances the storage instead of moving the tail"); check(q->present == (uint8_t *)(block + cap) + 1, "states keep following the whole value area"); check(q->filled == 63, "shift keeps the hole-free prefix"); for (int i = 1; i < 40; i++) scr_arr_shift_f64(q); check(q->data == block + 40 && q->len == 24 && scr_arr_get_f64(q, 0) == 40 && scr_arr_get_f64(q, 23) == 63, "repeated shifts index from the new front"); check_f64(scr_arr_unshift_f64(q, -1), 25, "unshift into head room"); check(q->data == block + 39 && scr_arr_get_f64(q, 0) == -1 && scr_arr_get_f64(q, 1) == 40, "unshift reuses the room shift left"); /* Appends past the window move the elements back to the front of the * same allocation when the reclaimed room is large. */ while (q->len < q->cap) scr_arr_push_f64(q, 100); scr_arr_push_f64(q, 101); check(q->data == block && q->cap == cap && q->len == 26, "growth reuses head room in place"); check(scr_arr_get_f64(q, 0) == -1 && scr_arr_get_f64(q, 1) == 40 && scr_arr_get_f64(q, q->len - 1) == 101 && !scr_arr_has(q, (double)q->len), "elements keep their order across the move"); /* A drained array starts over at the front of its allocation. */ while (q->len > 0) scr_arr_shift_f64(q); check(q->data == block && q->cap == cap && q->filled == 0, "draining resets the head room"); scr_arr_push_f64(q, 5); check(q->data == block && scr_arr_get_f64(q, 0) == 5, "a drained array refills from the front"); scr_arr_release(q); /* Small head room relative to the elements grows the allocation. */ ScrArr *full = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 64; i++) scr_arr_push_f64(full, i); while (full->len < full->cap) scr_arr_push_f64(full, 0); size_t full_cap = full->cap; scr_arr_shift_f64(full); scr_arr_push_f64(full, 1); scr_arr_push_f64(full, 2); check(full->cap > full_cap && scr_arr_get_f64(full, 0) == 1 && scr_arr_get_f64(full, full->len - 1) == 2, "growth past a small head room is geometric"); scr_arr_release(full); /* splice(0, n) as a value and as a statement. */ ScrArr *s = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 20; i++) scr_arr_push_f64(s, i); uint64_t *sblock = s->data; ScrArr *removed = scr_arr_splice(s, 0, 3); check(removed->len == 3 && scr_arr_get_f64(removed, 2) == 2 && s->data == sblock + 3 && scr_arr_get_f64(s, 0) == 3, "splice(0, n) leaves head room"); scr_arr_release(removed); scr_arr_splice_drop(s, 0, 2, 0, NULL); check(s->data == sblock + 5 && s->len == 15 && scr_arr_get_f64(s, 0) == 5, "statement splice(0, n) leaves head room"); /* Sparse writes far past the end, then growth absorbs them. */ scr_arr_set_f64(s, 2000000, 7); check(arr_sparse_len(s) == 1 && scr_arr_get_f64(s, 2000000) == 7, "sparse write after head room"); check_f64(scr_arr_shift_f64(s), 5, "shift of a sparse array"); check(scr_arr_get_f64(s, 1999999) == 7 && !scr_arr_has(s, 2000000), "shift renumbers sparse entries"); scr_arr_release(s); /* Strings: shifted references move out; the rest are released once. */ ScrArr *names = scr_arr_new(SCR_ELEM_STR, 0); for (int i = 0; i < 30; i++) scr_arr_push_ref(names, scr_str_new("name", 4)); for (int i = 0; i < 10; i++) scr_str_release((ScrStr *)scr_arr_shift_ref(names)); scr_arr_release(scr_arr_splice(names, 0, 5)); scr_arr_splice_drop(names, 0, 5, 0, NULL); scr_arr_unshift_ref(names, scr_str_new("front", 5)); ScrStr *front = (ScrStr *)scr_arr_get_ref(names, 0); check(strcmp(front->data, "front") == 0 && names->len == 11, "string queue keeps its order"); scr_str_release(front); scr_arr_release(names); /* A run of unshifts opens head room once it has moved the elements. */ ScrArr *rev = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 1000; i++) scr_arr_unshift_f64(rev, i); bool ordered = rev->len == 1000; for (int i = 0; i < 1000 && ordered; i++) ordered = scr_arr_get_f64(rev, i) == 999 - i; check(ordered, "unshift run keeps reverse order"); size_t room = (size_t)(rev->present - (uint8_t *)(rev->data + rev->cap)); check(room >= 4 && rev->cap >= rev->len, "an unshift run reserves head room"); uint64_t *rblock = rev->data; scr_arr_unshift_f64(rev, -1); check(rev->data == rblock - 1, "unshift into reserved head room needs no move"); uint64_t slots[3] = {0}; double v = 7; for (int i = 0; i < 3; i++) memcpy(&slots[i], &v, sizeof v); check_f64(scr_arr_unshift_many(rev, 3, slots), 1004, "unshift many into head room"); check(rev->data == rblock - 4 && scr_arr_get_f64(rev, 3) == -1, "unshift many order"); /* Storage swaps (typed views) first drop the head room. */ scr_arr_outline(rev); check(rev->present == (uint8_t *)(rev->data + rev->cap) && scr_arr_get_f64(rev, 0) == 7 && scr_arr_get_f64(rev, 1003) == 0, "outline moves the elements to the front of their allocation"); scr_arr_release(rev); /* A one-off unshift and small arrays keep moving their elements. */ ScrArr *once = scr_arr_new(SCR_ELEM_F64, 0); for (int i = 0; i < 40; i++) scr_arr_push_f64(once, i); size_t once_cap = once->cap; scr_arr_unshift_f64(once, -1); check(once->cap == once_cap && once->present == (uint8_t *)(once->data + once->cap), "a one-off unshift keeps the allocation"); scr_arr_release(once); /* Booleans move their elements (one-byte slots have no head room). */ ScrArr *flags = scr_arr_new(SCR_ELEM_BOOL, 0); for (int i = 0; i < 10; i++) scr_arr_push_bool(flags, i % 3 == 0); uint64_t *fblock = flags->data; check(scr_arr_shift_bool(flags) && !scr_arr_shift_bool(flags) && flags->data == fblock && flags->len == 8 && !scr_arr_get_bool(flags, 0) && scr_arr_get_bool(flags, 1), "boolean shift moves the tail"); scr_arr_release(flags); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "head room: no strings leaked"); check(scr_arr_live_count() == arrays0, "head room: no arrays leaked"); #endif } int main(int argc, char **argv) { if (argc > 1) { ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); scr_arr_push_f64(a, 1); if (strcmp(argv[1], "--crash-get-oob") == 0) { scr_arr_get_f64(a, 1); /* len is 1 */ } else if (strcmp(argv[1], "--crash-get-frac") == 0) { scr_arr_get_f64(a, 0.5); } else if (strcmp(argv[1], "--crash-hole-read") == 0) { scr_arr_set_f64(a, 2, 9); scr_arr_get_f64(a, 1); /* present-length read of a hole */ } else if (strcmp(argv[1], "--crash-pop-empty") == 0) { scr_arr_pop_f64(a); scr_arr_pop_f64(a); /* now empty */ } else { fprintf(stderr, "unknown mode %s\n", argv[1]); return 2; } fprintf(stderr, "expected a trap, still alive\n"); return 2; } test_primitive_sort(); test_comparator_sort(); test_keyed_sort(); test_dense_slots(); test_typed_sort(); test_dense_comparator_sort(); test_f64_basics(); test_numeric_read(); test_borrowed_ref_read(); test_bool(); test_exact_construction(); test_dense_replacement(); test_unshift_reverse(); test_str_rc(); test_nested_rc(); test_index_of_includes(); test_ref_elements(); test_ref_cycle(); test_ref_truncate_cycle(); test_ref_trace_storage_boundaries(); test_join(); test_sparse_holes(); test_large_storage(); test_bulk_reference_ownership(); test_splice_drop(); test_filled_prefix(); test_bool_bytes(); test_inline_slots(); test_head_room(); fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); return failed == 0 ? 0 : 1; }