mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-13 02:23:10 +08:00
863 lines
34 KiB
C
863 lines
34 KiB
C
#include "scr_runtime.h"
|
|
#include <assert.h>
|
|
#include <math.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
long scr_str_live_count(void);
|
|
long scr_dyn_key_live_count(void);
|
|
#endif
|
|
|
|
static void put(ScrDyn *map, ScrDyn *key, ScrDyn *value) {
|
|
ScrDyn *args[] = {key, value};
|
|
ScrDyn *result = scr_dyn_handle_ops_of(map)->invoke(map->v.handle.ptr, map, "set", args, 2, "map.set");
|
|
assert(result == map && !scr_exc_pending());
|
|
scr_dyn_release(result);
|
|
}
|
|
|
|
static void erase(ScrDyn *map, ScrDyn *key) {
|
|
ScrDyn *result = scr_dyn_handle_ops_of(map)->invoke(map->v.handle.ptr, map, "delete", &key, 1, "map.delete");
|
|
assert(result && result->kind == SCR_DYN_BOOL && result->v.b && !scr_exc_pending());
|
|
scr_dyn_release(result);
|
|
}
|
|
|
|
static ScrDyn *nothing(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
|
(void)closure; (void)args; (void)argc;
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
|
|
static void json_string_boundaries(void) {
|
|
/* Every escape/control at every word alignment, followed by raw UTF-8.
|
|
* ASan also checks short tails that cannot supply a complete word. */
|
|
const char suffix[] = "tail\xF0\x9F\x99\x82\xE9\x9B\xAA";
|
|
for (size_t prefix = 0; prefix < 64; prefix++) {
|
|
for (unsigned special = 0; special < 34; special++) {
|
|
char bytes[96];
|
|
memset(bytes, 'a', prefix);
|
|
bytes[prefix] = special < 32 ? (char)special : special == 32 ? '"' : '\\';
|
|
memcpy(bytes + prefix + 1, suffix, sizeof suffix - 1);
|
|
size_t length = prefix + sizeof suffix;
|
|
ScrStr *original = scr_str_new(bytes, length);
|
|
ScrJsonBuf buffer;
|
|
scr_jb_init(&buffer);
|
|
scr_jb_put_json_str(&buffer, original);
|
|
ScrStr *encoded = scr_jb_finish(&buffer);
|
|
ScrDyn *parsed = scr_json_parse(encoded);
|
|
assert(parsed && parsed->kind == SCR_DYN_STR && !scr_exc_pending());
|
|
assert(scr_str_eq(original, parsed->v.str));
|
|
scr_dyn_release(parsed);
|
|
scr_str_release(encoded);
|
|
scr_str_release(original);
|
|
|
|
if (special < 32) {
|
|
char invalid[68];
|
|
invalid[0] = '"';
|
|
memset(invalid + 1, 'a', prefix);
|
|
invalid[prefix + 1] = (char)special;
|
|
invalid[prefix + 2] = '"';
|
|
ScrStr *input = scr_str_new(invalid, prefix + 3);
|
|
assert(!scr_json_parse(input) && scr_exc_pending());
|
|
scr_exc_clear();
|
|
scr_str_release(input);
|
|
}
|
|
}
|
|
char plain[68];
|
|
plain[0] = '"';
|
|
memset(plain + 1, 'z', prefix);
|
|
plain[prefix + 1] = '"';
|
|
ScrStr *input = scr_str_new(plain, prefix + 2);
|
|
ScrDyn *parsed = scr_json_parse(input);
|
|
assert(parsed && parsed->v.str->len == prefix && !scr_exc_pending());
|
|
scr_dyn_release(parsed);
|
|
scr_str_release(input);
|
|
input = scr_str_new(plain, prefix + 1);
|
|
assert(!scr_json_parse(input) && scr_exc_pending());
|
|
scr_exc_clear();
|
|
scr_str_release(input);
|
|
}
|
|
/* Lone surrogates escape as \udXXX at every alignment and parse back
|
|
* lone; U+D7FF shares their lead byte and stays raw. */
|
|
scr_wtf8_note();
|
|
static const char *const units[] = {"\xED\xA0\xBD", "\xED\xB8\x80", "\xED\x9F\xBF"};
|
|
static const char *const escapes[] = {"\\ud83d", "\\ude00", "\xED\x9F\xBF"};
|
|
for (size_t prefix = 0; prefix < 24; prefix++) {
|
|
for (size_t u = 0; u < 3; u++) {
|
|
char bytes[64];
|
|
memset(bytes, 'b', prefix);
|
|
memcpy(bytes + prefix, units[u], 3);
|
|
memcpy(bytes + prefix + 3, "\xC3\xA9xyz", 5);
|
|
ScrStr *original = scr_str_new(bytes, prefix + 8);
|
|
ScrJsonBuf buffer;
|
|
scr_jb_init(&buffer);
|
|
scr_jb_put_json_str(&buffer, original);
|
|
ScrStr *encoded = scr_jb_finish(&buffer);
|
|
size_t escape_len = strlen(escapes[u]);
|
|
assert(encoded->len == prefix + 7 + escape_len);
|
|
assert(memcmp(encoded->data + 1 + prefix, escapes[u], escape_len) == 0);
|
|
ScrDyn *parsed = scr_json_parse(encoded);
|
|
assert(parsed && parsed->kind == SCR_DYN_STR && scr_str_eq(original, parsed->v.str));
|
|
scr_dyn_release(parsed);
|
|
scr_str_release(encoded);
|
|
scr_str_release(original);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void json_indent_ownership(void) {
|
|
const char compact[] = "{\"a\":[],\"b\":[1,{}]}";
|
|
const char expected[] = "{\n \"a\": [],\n \"b\": [\n 1,\n {}\n ]\n}";
|
|
ScrStr *input = scr_str_new(compact, sizeof compact - 1);
|
|
ScrStr *same = scr_json_indent(input, "", 0);
|
|
assert(same == input && input->rc == 2);
|
|
scr_str_release(same);
|
|
ScrStr *pretty = scr_json_indent(input, " ", 2);
|
|
assert(pretty->len == sizeof expected - 1);
|
|
assert(!memcmp(pretty->data, expected, sizeof expected - 1));
|
|
scr_str_release(input);
|
|
assert(!memcmp(pretty->data, expected, sizeof expected - 1));
|
|
scr_str_release(pretty);
|
|
}
|
|
|
|
static void checked_storage(void) {
|
|
/* Reuse after deletion, changing field order, embedded NUL and escaped
|
|
* duplicate names must leave independently owned keys and values. */
|
|
for (int round = 0; round < 100; round++) {
|
|
const char *text = round % 2 ? "{\"alpha\":1,\"beta\":2,\"gamma\":3}"
|
|
: "{\"gamma\":3,\"alpha\":0,\"\\u0061lpha\":1,\"beta\":2}";
|
|
ScrStr *input = scr_str_new(text, strlen(text));
|
|
ScrDyn *object = scr_json_parse(input);
|
|
scr_str_release(input);
|
|
assert(object && object->v.obj.len == 3);
|
|
assert(scr_dyn_obj_get(object, "alpha", 5)->v.num == 1);
|
|
ScrStr *key = scr_str_new("beta", 4);
|
|
scr_dyn_key_delete(object, key, true);
|
|
scr_str_release(key);
|
|
assert(!scr_dyn_obj_get(object, "beta", 4));
|
|
assert(scr_dyn_obj_get(object, "gamma", 5)->v.num == 3);
|
|
char mutable_key[] = "new\0key";
|
|
scr_dyn_obj_set(object, mutable_key, sizeof mutable_key - 1, scr_dyn_new_num(4));
|
|
mutable_key[0] = 'x';
|
|
assert(scr_dyn_obj_get(object, "new\0key", 7)->v.num == 4);
|
|
scr_dyn_release(object);
|
|
}
|
|
/* Larger objects and long names leave no stale pooled tail keys when a
|
|
* later small object grows through the same capacity. */
|
|
for (int round = 0; round < 4; round++) {
|
|
ScrDyn *object = scr_dyn_new_obj();
|
|
for (int i = 0; i < 80; i++) {
|
|
char key[160];
|
|
memset(key, 'a' + i % 20, sizeof key);
|
|
int prefix = snprintf(key, sizeof key, "%d:", i);
|
|
memset(key + prefix, 'a' + i % 20, sizeof key - (size_t)prefix);
|
|
scr_dyn_obj_set(object, key, sizeof key, scr_dyn_new_num(i));
|
|
assert(scr_dyn_obj_get(object, key, sizeof key)->v.num == i);
|
|
}
|
|
scr_dyn_release(object);
|
|
}
|
|
/* Cross index growth boundaries, invalidate moved entry positions, then
|
|
* recreate the index from survivors. Repeat after pool reuse. */
|
|
for (int round = 0; round < 4; round++) {
|
|
ScrDyn *object = scr_dyn_new_obj();
|
|
assert(!object->v.obj.index);
|
|
for (int i = 0; i < 129; i++) {
|
|
char key[16];
|
|
int length = snprintf(key, sizeof key, "key-%d", i);
|
|
scr_dyn_obj_set(object, key, (size_t)length, scr_dyn_new_num(i));
|
|
assert((object->v.obj.index != NULL) == (i >= 31));
|
|
}
|
|
for (int i = 0; i < 129; i++) {
|
|
char key[16];
|
|
int length = snprintf(key, sizeof key, "key-%d", i);
|
|
assert(scr_dyn_obj_get(object, key, (size_t)length)->v.num == i);
|
|
if (i % 3 == 0) {
|
|
ScrStr *name = scr_str_new(key, (size_t)length);
|
|
scr_dyn_key_delete(object, name, true);
|
|
scr_str_release(name);
|
|
assert(!scr_dyn_obj_get(object, key, (size_t)length));
|
|
}
|
|
}
|
|
scr_dyn_obj_set(object, "last", 4, scr_dyn_new_num(129));
|
|
assert(object->v.obj.index);
|
|
for (int i = 0; i < 129; i++) {
|
|
char key[16];
|
|
int length = snprintf(key, sizeof key, "key-%d", i);
|
|
ScrDyn *value = scr_dyn_obj_get(object, key, (size_t)length);
|
|
assert(i % 3 == 0 ? value == NULL : value && value->v.num == i);
|
|
}
|
|
scr_dyn_obj_set(object, "key-128", 7, scr_dyn_new_num(900));
|
|
assert(object->v.obj.len == 87);
|
|
assert(scr_dyn_obj_get(object, "key-128", 7)->v.num == 900);
|
|
scr_dyn_release(object);
|
|
}
|
|
/* A direct step is authorized only by the captured builtin method. */
|
|
ScrDyn *source = scr_dyn_new_arr();
|
|
scr_dyn_arr_push(source, scr_dyn_new_num(7));
|
|
scr_dyn_arr_push(source, scr_dyn_retain(scr_dyn_undefined()));
|
|
ScrStr *spell = scr_str_new("source", 6);
|
|
ScrDyn *iterator = scr_dyn_iterator(source, spell);
|
|
scr_str_release(spell);
|
|
ScrDyn *next = scr_dyn_handle_ops_of(iterator)->get(iterator->v.handle.ptr, "next", 4);
|
|
assert(scr_dyn_iterator_can_step(iterator, next));
|
|
assert(!scr_dyn_iterator_can_step(source, next));
|
|
assert(!scr_dyn_iterator_can_step(iterator, scr_dyn_undefined()));
|
|
ScrDyn *item = scr_dyn_iterator_step(iterator);
|
|
assert(item->v.num == 7 && !scr_dyn_iterator_step_done(iterator));
|
|
scr_dyn_release(item);
|
|
item = scr_dyn_iterator_step(iterator);
|
|
assert(item->kind == SCR_DYN_UNDEF && !scr_dyn_iterator_step_done(iterator));
|
|
scr_dyn_release(item);
|
|
scr_dyn_arr_push(source, scr_dyn_new_num(9));
|
|
item = scr_dyn_iterator_step(iterator);
|
|
assert(item->v.num == 9 && !scr_dyn_iterator_step_done(iterator));
|
|
scr_dyn_release(item);
|
|
item = scr_dyn_iterator_step(iterator);
|
|
assert(item->kind == SCR_DYN_UNDEF && scr_dyn_iterator_step_done(iterator));
|
|
scr_dyn_release(item);
|
|
scr_dyn_arr_push(source, scr_dyn_new_num(11));
|
|
item = scr_dyn_iterator_step(iterator);
|
|
assert(item->kind == SCR_DYN_UNDEF && scr_dyn_iterator_step_done(iterator));
|
|
scr_dyn_release(item);
|
|
scr_dyn_release(next);
|
|
scr_dyn_release(iterator);
|
|
scr_dyn_release(source);
|
|
assert(!scr_exc_pending());
|
|
}
|
|
|
|
static void shared_property_keys(void) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long before = scr_dyn_key_live_count();
|
|
#endif
|
|
/* Distinct objects share bytes, not values or property attributes. Key
|
|
* deletion, a collected owner and a caller-owned input cannot invalidate
|
|
* the surviving entry. */
|
|
ScrDyn *first = scr_dyn_new_obj(), *second = scr_dyn_new_obj();
|
|
char key[] = "shared\0name";
|
|
scr_dyn_obj_set(first, key, sizeof key - 1, scr_dyn_new_num(1));
|
|
scr_dyn_obj_set(second, key, sizeof key - 1, scr_dyn_new_num(2));
|
|
assert(first->v.obj.entries[0].key == second->v.obj.entries[0].key);
|
|
key[0] = 'x';
|
|
ScrStr *name = scr_str_new("shared\0name", sizeof key - 1);
|
|
scr_dyn_key_delete(first, name, true);
|
|
scr_str_release(name);
|
|
assert(scr_dyn_obj_get(second, "shared\0name", sizeof key - 1)->v.num == 2);
|
|
scr_dyn_obj_set(first, "shared\0name", sizeof key - 1, scr_dyn_new_num(3));
|
|
scr_dyn_obj_set(first, "self", 4, scr_dyn_retain(first));
|
|
scr_dyn_release(first);
|
|
scr_collect_cycles();
|
|
assert(scr_dyn_obj_get(second, "shared\0name", sizeof key - 1)->v.num == 2);
|
|
scr_dyn_release(second);
|
|
|
|
/* More distinct live names than weak slots force collisions. Releasing
|
|
* an evicted owner must not clear or free the replacement's storage. */
|
|
ScrDyn *objects[600];
|
|
for (int round = 0; round < 2; round++) {
|
|
for (int i = 0; i < 600; i++) {
|
|
char bytes[32];
|
|
int length = snprintf(bytes, sizeof bytes, "field-%d", i);
|
|
objects[i] = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(objects[i], bytes, (size_t)length, scr_dyn_new_num(i));
|
|
}
|
|
for (int i = 0; i < 600; i++) {
|
|
char bytes[32];
|
|
int length = snprintf(bytes, sizeof bytes, "field-%d", i);
|
|
assert(scr_dyn_obj_get(objects[i], bytes, (size_t)length)->v.num == i);
|
|
scr_dyn_release(objects[i]);
|
|
}
|
|
}
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_dyn_key_live_count() == before);
|
|
#endif
|
|
}
|
|
|
|
static void checked_leaf_cycles(void) {
|
|
/* Object properties omit primitives from trial deletion, while arrays and
|
|
* captured unknown values still trace them. Exercise shared leaves through
|
|
* both edge kinds, with and without an external owner, in every generation. */
|
|
for (uint16_t gen = SCR_CYC_NURSERY; gen < SCR_CYC_NGENS; gen++) {
|
|
for (int external = 0; external < 2; external++) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long before_dyns = scr_dyn_live_count();
|
|
long before_strings = scr_str_live_count();
|
|
#endif
|
|
ScrStr *text = scr_str_new("survives either edge", 20);
|
|
ScrDyn *leaf = scr_dyn_new_str(text);
|
|
scr_str_release(text);
|
|
scr_cyc_set_gen(scr_cyc_hdr(leaf), gen);
|
|
ScrDyn *first = scr_dyn_new_obj(), *second = scr_dyn_new_obj();
|
|
ScrDyn *array = scr_dyn_new_arr();
|
|
ScrClosure *closure = scr_closure_new(NULL, 1);
|
|
closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
|
|
scr_box_set_ref(closure->caps[0], scr_dyn_retain(leaf));
|
|
scr_dyn_obj_set(first, "callback", 8, scr_dyn_new_func(closure, nothing, 0, "func()=>dyn", "leaf"));
|
|
scr_dyn_obj_set(first, "leaf", 4, scr_dyn_retain(leaf));
|
|
scr_dyn_obj_set(second, "leaf", 4, scr_dyn_retain(leaf));
|
|
scr_dyn_obj_set(second, "number", 6, scr_dyn_new_num(42));
|
|
scr_dyn_obj_set(second, "empty", 5, scr_dyn_new_null());
|
|
scr_dyn_obj_set(second, "missing", 7, scr_dyn_retain(scr_dyn_undefined()));
|
|
scr_dyn_arr_push(array, scr_dyn_retain(leaf));
|
|
scr_dyn_arr_push(array, scr_dyn_retain(first));
|
|
scr_dyn_obj_set(first, "next", 4, scr_dyn_retain(second));
|
|
scr_dyn_obj_set(second, "back", 4, scr_dyn_retain(first));
|
|
scr_dyn_obj_set(second, "array", 5, array);
|
|
if (!external) scr_dyn_release(leaf);
|
|
scr_dyn_release(first);
|
|
scr_dyn_release(second);
|
|
scr_cyc_collect_scheduled();
|
|
scr_collect_cycles();
|
|
if (external) {
|
|
assert(leaf->rc == 1 && leaf->v.str->len == 20);
|
|
scr_dyn_release(leaf);
|
|
}
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_dyn_live_count() == before_dyns);
|
|
assert(scr_str_live_count() == before_strings);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/* A previous trace snapshot must not survive a property mutation: the
|
|
* next collection must trace a new object edge or release a new leaf. */
|
|
for (int leaf_after = 0; leaf_after < 2; leaf_after++) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long before = scr_dyn_live_count();
|
|
#endif
|
|
ScrDyn *object = scr_dyn_new_obj(), *child = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(object, "self", 4, scr_dyn_retain(object));
|
|
scr_dyn_obj_set(object, "value", 5, leaf_after ? scr_dyn_retain(child) : scr_dyn_new_num(1));
|
|
scr_dyn_release(scr_dyn_retain(object));
|
|
scr_collect_cycles();
|
|
scr_dyn_obj_set(object, "value", 5, leaf_after ? scr_dyn_new_num(2) : scr_dyn_retain(child));
|
|
scr_dyn_obj_set(child, "parent", 6, scr_dyn_retain(object));
|
|
scr_dyn_release(child);
|
|
scr_dyn_release(object);
|
|
scr_collect_cycles();
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_dyn_live_count() == before);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static void checked_number_storage(void) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long before = scr_dyn_live_count();
|
|
#endif
|
|
for (int value = -130; value < 258; value++) {
|
|
ScrDyn *first = scr_dyn_new_num(value), *second = scr_dyn_new_num(value);
|
|
assert(first->kind == SCR_DYN_NUM && first->v.num == value);
|
|
assert(scr_dyn_strict_eq(first, second));
|
|
if (value >= -128 && value <= 255) assert(first == second && first->rc == SIZE_MAX);
|
|
ScrDyn *array = scr_dyn_new_arr();
|
|
scr_dyn_arr_push(array, first);
|
|
scr_dyn_arr_push(array, second);
|
|
scr_dyn_arr_push(array, scr_dyn_retain(array));
|
|
scr_dyn_release(array);
|
|
scr_collect_cycles();
|
|
}
|
|
const double special[] = {-0.0, 0.5, -0.5, NAN, INFINITY, -INFINITY, 9007199254740991.0};
|
|
for (size_t i = 0; i < sizeof special / sizeof special[0]; i++) {
|
|
ScrDyn *value = scr_dyn_new_num(special[i]);
|
|
assert(value->rc != SIZE_MAX);
|
|
assert(isnan(special[i]) ? isnan(value->v.num) : value->v.num == special[i]);
|
|
if (special[i] == 0) assert(signbit(value->v.num));
|
|
scr_dyn_release(value);
|
|
}
|
|
const char raw[] = "[-0,0,1.0,1e2,-128,255,256,1e400]";
|
|
ScrStr *input = scr_str_new(raw, sizeof raw - 1);
|
|
ScrDyn *parsed = scr_json_parse(input);
|
|
assert(parsed && parsed->v.arr.len == 8);
|
|
assert(signbit(parsed->v.arr.items[0]->v.num));
|
|
assert(!signbit(parsed->v.arr.items[1]->v.num));
|
|
assert(parsed->v.arr.items[2] == scr_dyn_new_num(1));
|
|
assert(parsed->v.arr.items[3] == scr_dyn_new_num(100));
|
|
assert(isinf(parsed->v.arr.items[7]->v.num));
|
|
scr_dyn_release(parsed);
|
|
scr_str_release(input);
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_dyn_live_count() == before);
|
|
#endif
|
|
}
|
|
|
|
/* ── intact parse regions ───────────────────────────────────────────── */
|
|
|
|
static bool buffered(ScrDyn *d) { return scr_cyc_is_buffered(scr_cyc_hdr(d)); }
|
|
|
|
/* Drop a temporary reference the way member reads do. */
|
|
static bool buffered_after_read(ScrDyn *d) {
|
|
scr_dyn_release(scr_dyn_retain(d));
|
|
return buffered(d);
|
|
}
|
|
|
|
static ScrDyn *parse_text(const char *text) {
|
|
ScrStr *input = scr_str_new(text, strlen(text));
|
|
ScrDyn *parsed = scr_json_parse(input);
|
|
scr_str_release(input);
|
|
assert(parsed && !scr_exc_pending());
|
|
return parsed;
|
|
}
|
|
|
|
static const char region_doc[] = "{\"a\":{\"b\":[1,{\"c\":2}],\"s\":\"text\"},\"n\":300}";
|
|
|
|
/* Store a reference to `root` into a member of its own document through one
|
|
* of the barriered writers. */
|
|
static void close_cycle(ScrDyn *root, int writer) {
|
|
ScrDyn *a = scr_dyn_obj_get(root, "a", 1);
|
|
ScrDyn *b = scr_dyn_obj_get(a, "b", 1);
|
|
switch (writer) {
|
|
case 0: scr_dyn_obj_set(a, "parent", 6, scr_dyn_retain(root)); break;
|
|
case 1: scr_dyn_arr_push(b, scr_dyn_retain(root)); break;
|
|
case 2: {
|
|
ScrStr *key = scr_str_new("0", 1);
|
|
scr_dyn_key_set(b, key, root);
|
|
scr_str_release(key);
|
|
break;
|
|
}
|
|
case 3: {
|
|
ScrStr *key = scr_str_new("extra", 5);
|
|
scr_dyn_key_set(b, key, root);
|
|
scr_str_release(key);
|
|
break;
|
|
}
|
|
case 4: scr_dyn_release(scr_dyn_set_prototype(a, root)); break;
|
|
case 5: {
|
|
ScrDyn *descriptor = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_retain(root));
|
|
ScrStr *text = scr_str_new("p", 1);
|
|
ScrDyn *key = scr_dyn_new_str(text);
|
|
scr_str_release(text);
|
|
scr_dyn_release(scr_dyn_define_property(a, key, descriptor));
|
|
scr_dyn_release(key);
|
|
scr_dyn_release(descriptor);
|
|
break;
|
|
}
|
|
}
|
|
assert(!scr_exc_pending());
|
|
}
|
|
|
|
static void json_intact_regions(void) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long before = scr_dyn_live_count();
|
|
#endif
|
|
/* Member reads of an intact document buffer nothing, and primitive
|
|
* stores keep it intact. */
|
|
ScrDyn *root = parse_text(region_doc);
|
|
ScrDyn *a = scr_dyn_obj_get(root, "a", 1);
|
|
ScrDyn *b = scr_dyn_obj_get(a, "b", 1);
|
|
assert(!buffered_after_read(root) && !buffered_after_read(a) && !buffered_after_read(b));
|
|
assert(!buffered_after_read(b->v.arr.items[1]));
|
|
scr_dyn_obj_set(a, "n", 1, scr_dyn_new_num(1000.5));
|
|
scr_dyn_arr_push(b, scr_dyn_new_null());
|
|
scr_dyn_obj_set(a, "u", 1, scr_dyn_retain(scr_dyn_undefined()));
|
|
assert(!buffered_after_read(a) && !buffered_after_read(b));
|
|
|
|
/* A store of another document's node invalidates only the written one. */
|
|
ScrDyn *other = parse_text(region_doc);
|
|
scr_dyn_obj_set(a, "other", 5, scr_dyn_retain(other));
|
|
assert(!buffered_after_read(scr_dyn_obj_get(other, "a", 1)));
|
|
assert(buffered_after_read(root));
|
|
scr_dyn_release(other);
|
|
scr_dyn_release(root);
|
|
scr_collect_cycles();
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_dyn_live_count() == before);
|
|
#endif
|
|
|
|
/* The cycle runs through the root, which was never written. A pass while
|
|
* the root is still referenced finds everything alive and drains the
|
|
* candidates; the later release of the root alone must buffer it again. */
|
|
for (int writer = 0; writer < 6; writer++) {
|
|
root = parse_text(region_doc);
|
|
close_cycle(root, writer);
|
|
assert(buffered_after_read(scr_dyn_obj_get(root, "a", 1)));
|
|
scr_collect_cycles();
|
|
assert(root->rc == 2);
|
|
scr_dyn_release(root);
|
|
assert(buffered(root));
|
|
scr_collect_cycles();
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_dyn_live_count() == before);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/* ── schema-directed parse ──────────────────────────────────────────── */
|
|
|
|
typedef struct {
|
|
size_t rc;
|
|
double qty;
|
|
ScrStr *sku;
|
|
uint8_t ok;
|
|
ScrArr *tags;
|
|
} TestItem;
|
|
|
|
static long test_items_live;
|
|
|
|
static void *test_item_new(void) {
|
|
TestItem *item = calloc(1, sizeof *item);
|
|
assert(item);
|
|
item->rc = 1;
|
|
test_items_live++;
|
|
return item;
|
|
}
|
|
|
|
static void *test_item_retain(void *ptr) {
|
|
((TestItem *)ptr)->rc++;
|
|
return ptr;
|
|
}
|
|
|
|
static void test_item_release(void *ptr) {
|
|
TestItem *item = ptr;
|
|
if (!item || --item->rc) return;
|
|
scr_str_release(item->sku);
|
|
scr_arr_release(item->tags);
|
|
test_items_live--;
|
|
scr_weak_dispose(item); /* emitted record frees notify the dispose hook */
|
|
free(item);
|
|
}
|
|
|
|
static ScrArr *test_tags_new(size_t cap) { return scr_arr_new(SCR_ELEM_STR, cap); }
|
|
static ScrArr *test_list_new(size_t cap) {
|
|
static const ScrArrOps item_ops = { test_item_retain, test_item_release, NULL };
|
|
return scr_arr_new_ops(&item_ops, cap);
|
|
}
|
|
static void test_arr_release(void *ptr) { scr_arr_release(ptr); }
|
|
|
|
static const ScrJsonSchema test_f64 = {SCR_JSCHEMA_F64, 0, NULL, NULL, NULL, NULL, NULL};
|
|
static const ScrJsonSchema test_bool = {SCR_JSCHEMA_BOOL, 0, NULL, NULL, NULL, NULL, NULL};
|
|
static const ScrJsonSchema test_str = {SCR_JSCHEMA_STR, 0, NULL, NULL, NULL, NULL, NULL};
|
|
static const ScrJsonSchema test_tags = {SCR_JSCHEMA_ARR, 0, NULL, &test_str, NULL, test_tags_new, test_arr_release};
|
|
static const ScrJsonSchemaField test_item_fields[] = {
|
|
{"sku", 3, offsetof(TestItem, sku), &test_str},
|
|
{"qty", 3, offsetof(TestItem, qty), &test_f64},
|
|
{"ok", 2, offsetof(TestItem, ok), &test_bool},
|
|
{"tags", 4, offsetof(TestItem, tags), &test_tags},
|
|
};
|
|
static const ScrJsonSchema test_item = {SCR_JSCHEMA_REC, 4, test_item_fields, NULL, test_item_new, NULL, test_item_release};
|
|
static const ScrJsonSchema test_list = {SCR_JSCHEMA_ARR, 0, NULL, &test_item, NULL, test_list_new, test_arr_release};
|
|
|
|
static bool str_is(const ScrStr *s, const char *text) {
|
|
return s->len == strlen(text) && memcmp(s->data, text, s->len) == 0;
|
|
}
|
|
|
|
static ScrArr *schema_parse(const char *text) {
|
|
ScrStr *input = scr_str_new(text, strlen(text));
|
|
ScrArr *list = scr_json_parse_schema(input, &test_list);
|
|
scr_str_release(input);
|
|
assert(!scr_exc_pending()); /* declining never throws */
|
|
return list;
|
|
}
|
|
|
|
/* Member names a direct text comparison must not match: a quote or a
|
|
* backslash in a name is written escaped in JSON text. */
|
|
typedef struct {
|
|
size_t rc;
|
|
double quoted, slashed;
|
|
} TestPair;
|
|
static int test_pairs_live = 0;
|
|
static void *test_pair_new(void) {
|
|
TestPair *pair = calloc(1, sizeof *pair);
|
|
assert(pair);
|
|
pair->rc = 1;
|
|
test_pairs_live++;
|
|
return pair;
|
|
}
|
|
static void test_pair_release(void *ptr) {
|
|
TestPair *pair = ptr;
|
|
if (!pair || --pair->rc) return;
|
|
test_pairs_live--;
|
|
scr_weak_dispose(pair); /* emitted record frees notify the dispose hook */
|
|
free(pair);
|
|
}
|
|
static const ScrJsonSchemaField test_pair_fields[] = {
|
|
{"a\"b", 3, offsetof(TestPair, quoted), &test_f64},
|
|
{"c\\d", 3, offsetof(TestPair, slashed), &test_f64},
|
|
};
|
|
static const ScrJsonSchema test_pair = {SCR_JSCHEMA_REC, 2, test_pair_fields, NULL, test_pair_new, NULL, test_pair_release};
|
|
|
|
static TestPair *pair_parse(const char *text) {
|
|
ScrStr *input = scr_str_new(text, strlen(text));
|
|
TestPair *pair = scr_json_parse_schema(input, &test_pair);
|
|
scr_str_release(input);
|
|
assert(!scr_exc_pending());
|
|
return pair;
|
|
}
|
|
|
|
static void json_schema_special_names(void) {
|
|
TestPair *pair = pair_parse("{\"a\\\"b\":1,\"c\\\\d\":2}");
|
|
assert(pair && pair->quoted == 1 && pair->slashed == 2);
|
|
test_pair_release(pair);
|
|
pair = pair_parse("{\"c\\\\d\":4,\"a\\u0022b\":3}");
|
|
assert(pair && pair->quoted == 3 && pair->slashed == 4);
|
|
assert(scr_record_source_has(pair)); /* members out of declaration order */
|
|
test_pair_release(pair);
|
|
/* Raw bytes equal to a name are not that name: the key ends at the
|
|
* quote, and a lone backslash starts an escape. */
|
|
assert(pair_parse("{\"a\"b\":1,\"c\\\\d\":2}") == NULL);
|
|
assert(pair_parse("{\"a\\\"b\":1,\"c\\d\":2}") == NULL);
|
|
assert(test_pairs_live == 0);
|
|
}
|
|
|
|
static void json_schema_parse(void) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long strings = scr_str_live_count();
|
|
#endif
|
|
ScrArr *list = schema_parse(
|
|
" [ {\"sku\":\"a\",\"qty\":1.5,\"ok\":true,\"tags\":[\"x\",\"y\\n\"]} ,"
|
|
"{\"tags\":[],\"ok\":false,\"extra\":{\"n\":[1,\"s\\u00e9\",null,true,-2e-3,{}]},"
|
|
"\"qty\":-0,\"\\u0073ku\":\"\\ud83d\\ude00\"} ] ");
|
|
assert(list && list->len == 2 && test_items_live == 2);
|
|
TestItem *first = scr_arr_get_ref(list, 0);
|
|
assert(first->qty == 1.5 && first->ok == 1 && str_is(first->sku, "a"));
|
|
assert(first->tags->len == 2);
|
|
ScrStr *tag = scr_arr_get_ref(first->tags, 1);
|
|
assert(str_is(tag, "y\n"));
|
|
scr_str_release(tag);
|
|
test_item_release(first);
|
|
TestItem *second = scr_arr_get_ref(list, 1);
|
|
assert(second->qty == 0 && signbit(second->qty) && second->ok == 0 && second->tags->len == 0);
|
|
assert(str_is(second->sku, "\xF0\x9F\x98\x80"));
|
|
/* The undeclared key and the reordered members stay beside the record in
|
|
* source order; the record in declaration order keeps nothing. */
|
|
assert(!scr_record_source_has(first));
|
|
assert(scr_record_source_has(second));
|
|
const ScrDyn *view = scr_record_source_view(second);
|
|
static const char *const order[] = {"tags", "ok", "extra", "qty", "sku"};
|
|
assert(view->kind == SCR_DYN_OBJ && view->v.obj.len == 5);
|
|
for (size_t i = 0; i < 5; i++) {
|
|
assert(view->v.obj.entries[i].key_len == strlen(order[i]));
|
|
assert(memcmp(view->v.obj.entries[i].key, order[i], strlen(order[i])) == 0);
|
|
}
|
|
assert(scr_dyn_obj_get(view, "extra", 5)->kind == SCR_DYN_OBJ);
|
|
test_item_release(second);
|
|
assert(scr_record_source_has(second)); /* the list still holds it */
|
|
scr_arr_release(list);
|
|
assert(test_items_live == 0);
|
|
assert(!scr_record_source_has(second)); /* released with the record */
|
|
|
|
list = schema_parse("[]");
|
|
assert(list && list->len == 0);
|
|
scr_arr_release(list);
|
|
|
|
/* Inputs the checked-dynamic route might reject or read differently
|
|
* decline without throwing and release every partial value. */
|
|
static const char *const declined[] = {
|
|
"",
|
|
" ",
|
|
"{}",
|
|
"[null]",
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true}]", /* missing */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[],\"qty\":2}]", /* duplicate */
|
|
"[{\"sku\":\"a\",\"qty\":\"1\",\"ok\":true,\"tags\":[]}]", /* kind */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[\"t\",7]}]", /* nested kind */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[]},{\"sku\":\"b\"", /* truncated */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[]},]", /* trailing comma */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[]}] x", /* trailing content */
|
|
"[{\"sku\":\"a\",\"qty\":01,\"ok\":true,\"tags\":[]}]", /* number syntax */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":tru,\"tags\":[]}]", /* literal */
|
|
"[{\"sku\":\"a\\q\",\"qty\":1,\"ok\":true,\"tags\":[]}]", /* escape */
|
|
"[{\"sku\":\"a\",\"x\":\"\\u12\",\"qty\":1,\"ok\":true,\"tags\":[]}]", /* skipped escape */
|
|
"[{\"sku\":\"a\",\"x\":[1,],\"qty\":1,\"ok\":true,\"tags\":[]}]", /* skipped syntax */
|
|
"[{\"sku\":\"a\",\"x\":{\"k\" 1},\"qty\":1,\"ok\":true,\"tags\":[]}]",
|
|
"[{\"sku\":\"a\nb\",\"qty\":1,\"ok\":true,\"tags\":[]}]", /* control */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[]} {}]",
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[]},{\"sku\"", /* key at the end */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[]},{\"sk", /* cut key */
|
|
"[{\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[]},{\"sku", /* unclosed key */
|
|
};
|
|
for (size_t i = 0; i < sizeof declined / sizeof *declined; i++) {
|
|
assert(!schema_parse(declined[i]));
|
|
assert(test_items_live == 0);
|
|
}
|
|
|
|
/* Nesting beyond the checked parser's limit declines (it reports the
|
|
* RangeError); nesting within it is kept beside the record. */
|
|
for (int depth = 999; depth <= 1001; depth++) {
|
|
char text[2200];
|
|
size_t n = 0;
|
|
n += (size_t)snprintf(text + n, sizeof text - n, "[{\"x\":");
|
|
for (int i = 0; i < depth - 2; i++) text[n++] = '[';
|
|
for (int i = 0; i < depth - 2; i++) text[n++] = ']';
|
|
n += (size_t)snprintf(text + n, sizeof text - n, ",\"sku\":\"a\",\"qty\":1,\"ok\":true,\"tags\":[]}]");
|
|
ScrArr *parsed = schema_parse(text);
|
|
assert((parsed != NULL) == (depth <= 1000));
|
|
scr_arr_release(parsed);
|
|
}
|
|
assert(test_items_live == 0);
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_str_live_count() == strings);
|
|
#endif
|
|
}
|
|
|
|
int main(void) {
|
|
json_string_boundaries();
|
|
json_indent_ownership();
|
|
scr_init();
|
|
checked_storage();
|
|
shared_property_keys();
|
|
checked_leaf_cycles();
|
|
checked_number_storage();
|
|
json_intact_regions();
|
|
json_schema_parse();
|
|
json_schema_special_names();
|
|
/* Native Set boxes may own headerless scalar/string maps. Collecting an
|
|
* enclosing cycle must neither trace those leaves nor skip their release. */
|
|
for (int i = 0; i < 2000; i++) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long before_maps = scr_map_live_count();
|
|
long before_dyns = scr_dyn_live_count();
|
|
#endif
|
|
ScrMap *numbers = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL);
|
|
ScrMap *strings = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
|
scr_map_set_f64_f64(numbers, 42, 1);
|
|
ScrStr *text = scr_str_new("kept", 4);
|
|
scr_map_set_str_f64(strings, text, 1);
|
|
scr_str_release(text);
|
|
ScrDyn *object = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(object, "numbers", 7, scr_dyn_native_set(numbers));
|
|
scr_dyn_obj_set(object, "strings", 7, scr_dyn_native_set(strings));
|
|
scr_dyn_obj_set(object, "self", 4, scr_dyn_retain(object));
|
|
scr_map_release(strings); /* only the box owns this leaf */
|
|
scr_dyn_release(object);
|
|
scr_collect_cycles();
|
|
assert(numbers->rc == 1 && scr_map_has_f64(numbers, 42));
|
|
scr_map_release(numbers);
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_map_live_count() == before_maps);
|
|
assert(scr_dyn_live_count() == before_dyns);
|
|
#endif
|
|
}
|
|
/* A live alias survives collection; dropping it releases the entire
|
|
* object/closure/capture cycle, including its acyclic string leaf. */
|
|
for (int i = 0; i < 2000; i++) {
|
|
#ifdef SCR_RC_AUDIT
|
|
long before = scr_dyn_live_count();
|
|
#endif
|
|
ScrDyn *object = scr_dyn_new_obj();
|
|
ScrClosure *closure = scr_closure_new(NULL, 1);
|
|
closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
|
|
scr_box_set_ref(closure->caps[0], scr_dyn_retain(object));
|
|
ScrDyn *callback = scr_dyn_new_func(closure, nothing, 0, "func()=>dyn", "read");
|
|
scr_dyn_obj_set(object, "read", 4, callback);
|
|
ScrStr *text = scr_str_new("alive", 5);
|
|
scr_dyn_obj_set(object, "text", 4, scr_dyn_new_str(text));
|
|
scr_str_release(text);
|
|
ScrDyn *alias = scr_dyn_retain(object);
|
|
scr_dyn_release(object);
|
|
scr_collect_cycles();
|
|
assert(scr_dyn_obj_get(alias, "text", 4)->v.str->len == 5);
|
|
scr_dyn_release(alias);
|
|
scr_collect_cycles();
|
|
#ifdef SCR_RC_AUDIT
|
|
assert(scr_dyn_live_count() == before);
|
|
#endif
|
|
}
|
|
/* A checked bigint owns its payload independently of the producing slot. */
|
|
for (int i = 0; i < 2000; i++) {
|
|
ScrStr *decimal = scr_str_new("18446744073709551615", 20);
|
|
ScrBigInt *integer = scr_bigint_parse(decimal);
|
|
ScrDyn *boxed = scr_dyn_new_bigint(integer);
|
|
scr_bigint_release(integer);
|
|
ScrDyn *copy = scr_dyn_new_bigint(boxed->v.bigint);
|
|
assert(scr_dyn_strict_eq(boxed, copy) && scr_dyn_truthy(copy));
|
|
scr_dyn_release(boxed);
|
|
ScrStr *rendered = scr_dyn_to_string(copy, NULL);
|
|
assert(scr_str_eq(decimal, rendered));
|
|
scr_str_release(decimal);
|
|
scr_str_release(rendered);
|
|
scr_dyn_release(copy);
|
|
}
|
|
ScrDyn *snapshot = scr_dyn_mark_snapshot(scr_dyn_new_obj());
|
|
scr_dyn_release(snapshot);
|
|
ScrDyn *fresh = scr_dyn_new_obj();
|
|
assert(!fresh->copied_from_native);
|
|
scr_dyn_release(fresh);
|
|
ScrDyn *map = scr_weak_map_new(scr_dyn_undefined());
|
|
ScrDyn *key = scr_dyn_new_obj();
|
|
ScrDyn *first = scr_dyn_new_obj(), *second = scr_dyn_new_obj();
|
|
put(map, key, first);
|
|
assert(key->rc == 1 && first->rc == 2);
|
|
put(map, key, second);
|
|
assert(first->rc == 1 && second->rc == 2);
|
|
erase(map, key);
|
|
assert(second->rc == 1);
|
|
put(map, key, first);
|
|
put(map, second, first);
|
|
scr_dyn_release(key);
|
|
assert(first->rc == 2); /* first key died; second is still alive */
|
|
scr_dyn_release(second);
|
|
assert(first->rc == 1);
|
|
|
|
/* Releasing a value can dispose a second key and its value. */
|
|
key = scr_dyn_new_obj();
|
|
ScrDyn *inner = scr_dyn_new_obj();
|
|
put(map, key, inner);
|
|
put(map, inner, first);
|
|
scr_dyn_release(inner);
|
|
scr_dyn_release(key);
|
|
assert(first->rc == 1);
|
|
|
|
/* Multiple maps observe the same key without retaining it. */
|
|
ScrDyn *other_map = scr_weak_map_new(scr_dyn_undefined());
|
|
key = scr_dyn_new_arr();
|
|
put(map, key, first);
|
|
put(other_map, key, first);
|
|
assert(key->rc == 1 && first->rc == 3);
|
|
scr_dyn_release(other_map);
|
|
assert(key->rc == 1 && first->rc == 2);
|
|
scr_dyn_release(key);
|
|
assert(first->rc == 1);
|
|
|
|
/* View and backing buffer are distinct keys, including a root view.
|
|
* Dropping a box cannot remove metadata while native storage survives. */
|
|
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, 8);
|
|
key = scr_dyn_new_bytes(bytes);
|
|
ScrDyn *buffer = scr_array_buffer_from_bytes(bytes);
|
|
put(map, key, first);
|
|
put(map, buffer, first);
|
|
assert(first->rc == 3 && bytes->rc == 3);
|
|
scr_dyn_release(key);
|
|
scr_dyn_release(buffer);
|
|
assert(first->rc == 3 && bytes->rc == 1);
|
|
buffer = scr_array_buffer_from_bytes(bytes);
|
|
erase(map, buffer);
|
|
assert(first->rc == 2);
|
|
scr_dyn_release(buffer);
|
|
scr_bytes_release(bytes);
|
|
assert(first->rc == 1);
|
|
|
|
ScrClosure *closure = scr_closure_new(NULL, 0);
|
|
key = scr_dyn_new_func(scr_closure_retain(closure), nothing, 0, "", "key");
|
|
put(map, key, first);
|
|
scr_dyn_release(key);
|
|
assert(closure->rc == 1 && first->rc == 2);
|
|
scr_closure_release(closure);
|
|
assert(first->rc == 1);
|
|
|
|
/* A WeakMap can itself be a weak key. */
|
|
other_map = scr_weak_map_new(scr_dyn_undefined());
|
|
put(map, other_map, first);
|
|
scr_dyn_release(other_map);
|
|
assert(first->rc == 1);
|
|
|
|
ScrDyn *weak_set = scr_weak_set_new(scr_dyn_undefined());
|
|
key = scr_dyn_new_obj();
|
|
ScrDyn *added = scr_dyn_handle_ops_of(weak_set)->invoke(weak_set->v.handle.ptr, weak_set, "add", &key, 1, "set.add");
|
|
assert(added == weak_set && key->rc == 1);
|
|
scr_dyn_release(added);
|
|
scr_dyn_release(key);
|
|
scr_dyn_release(weak_set);
|
|
|
|
key = scr_dyn_new_obj();
|
|
put(map, key, first);
|
|
scr_dyn_release(map);
|
|
assert(key->rc == 1 && first->rc == 1);
|
|
scr_dyn_release(key);
|
|
scr_dyn_release(first);
|
|
assert(!scr_exc_pending());
|
|
puts("checked storage and weak metadata lifetime checks passed");
|
|
return 0;
|
|
}
|