mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-04 09:28:24 +08:00
- Reduce allocation overhead in array comparisons and string operations. - Reuse fiber stacks and release cached memory while idle. - Read terminal dimensions directly from native streams.
674 lines
25 KiB
C
674 lines
25 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>
|
|
|
|
#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++;
|
|
scr_cyc_mark_live(r);
|
|
}
|
|
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) {
|
|
MockRec *r = scr_cyc_alloc(sizeof *r, &mock_trace, &mock_gcfree);
|
|
r->rc = 1;
|
|
r->value = value;
|
|
mock_live++;
|
|
return r;
|
|
}
|
|
|
|
static void test_ref_elements(void) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long arrays0 = scr_arr_live_count();
|
|
#endif
|
|
ScrArr *a = scr_arr_new_ref(&mock_retain, &mock_release, NULL, 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_ref(&mock_cyc_retain, &mock_cyc_release, &mock_trace, 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_ref(&mock_cyc_retain, &mock_cyc_release, &mock_trace, 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_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);
|
|
|
|
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(high->sparse_len == 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(high->sparse_len == 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
|
|
}
|
|
|
|
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_f64_basics();
|
|
test_numeric_read();
|
|
test_bool();
|
|
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_join();
|
|
test_sparse_holes();
|
|
|
|
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
|
|
return failed == 0 ? 0 : 1;
|
|
}
|