Files
scriptc/packages/runtime/test/test_array.c
T

1761 lines
74 KiB
C

/* 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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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);
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");
}
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_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();
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
return failed == 0 ? 0 : 1;
}