Files
scriptc/packages/runtime/src/scr_message.test.c
T
Chris Tate b9cc0ed820 Sort the handle tags by name with explicit values
Every change that added a checked-dynamic handle kind appended its tag
just before SCR_DYNH_COUNT in scr_dyn.h, so any two such changes in
flight conflicted there. The tags are now sorted by name, each with its
current value written out, so a new tag goes in its sorted place with the
next free value, and changes adding different tags edit different lines.
SCR_DYNH_COUNT, used only by the capacity check, is gone.

The values are unchanged and the runtime artifacts are identical. A new
test checks what the enum's order used to guarantee: values are unique and
below SCR_DYNH_CAPACITY, and the LLVM backend's hardcoded tag numbers
(DYN_HANDLE_TAG_NUM) match the header.
2026-10-18 22:06:48 -05:00

305 lines
11 KiB
C

#undef NDEBUG
#include "scr_message.h"
#include <assert.h>
#include <math.h>
#include <pthread.h>
#include <stdio.h>
static void put_number(ScrDyn *object, const char *key, double value) {
scr_dyn_obj_set(object, key, strlen(key), scr_dyn_new_num(value));
}
static ScrDyn *make_graph(void) {
ScrDyn *root = scr_dyn_new_obj();
ScrDyn *shared = scr_dyn_new_obj();
put_number(shared, "value", 17);
scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(shared));
scr_dyn_obj_set(root, "second", 6, shared);
scr_dyn_obj_set(root, "self", 4, scr_dyn_retain(root));
put_number(root, "negativeZero", -0.0);
put_number(root, "nan", NAN);
put_number(root, "infinity", INFINITY);
ScrStr *text = scr_str_new("a\0\xf0\x9f\x98\x80", 6);
scr_dyn_obj_set(root, "text", 4, scr_dyn_new_str(text));
scr_str_release(text);
text = scr_str_new("123456789012345678901234567890", 30);
ScrBigInt *integer = scr_bigint_parse(text);
scr_str_release(text);
scr_dyn_obj_set(root, "integer", 7, scr_dyn_new_bigint(integer));
scr_bigint_release(integer);
ScrDyn *array = scr_dyn_new_arr();
scr_dyn_arr_push_hole(array);
scr_dyn_arr_push(array, scr_dyn_retain(scr_dyn_undefined()));
scr_dyn_arr_push_hole(array);
scr_dyn_arr_push(array, scr_dyn_retain(shared));
scr_dyn_obj_set(root, "array", 5, array);
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, 16);
for (size_t i = 0; i < bytes->len; i++) bytes->data[i] = (unsigned char)i;
ScrBytes *view = scr_bytes_buffer_view(bytes, SCR_BYTES_U16, 4, true, 3);
scr_dyn_obj_set(root, "bytes", 5, scr_dyn_new_bytes(view));
scr_dyn_obj_set(root, "again", 5, scr_dyn_new_bytes(view));
scr_dyn_obj_set(root, "buffer", 6, scr_array_buffer_from_bytes(bytes));
scr_bytes_release(view);
scr_bytes_release(bytes);
return root;
}
static void *receive_graph(void *data) {
scr_context_enter(1);
scr_init();
scr_lib_init(0, NULL);
ScrDyn *root = scr_message_decode(data);
assert(root && !scr_exc_pending());
ScrDyn *first = scr_dyn_obj_get(root, "first", 5);
assert(first == scr_dyn_obj_get(root, "second", 6));
assert(root == scr_dyn_obj_get(root, "self", 4));
assert(signbit(scr_dyn_obj_get(root, "negativeZero", 12)->v.num));
assert(isnan(scr_dyn_obj_get(root, "nan", 3)->v.num));
assert(isinf(scr_dyn_obj_get(root, "infinity", 8)->v.num));
ScrStr *text = scr_dyn_obj_get(root, "text", 4)->v.str;
assert(text->len == 6 && !memcmp(text->data, "a\0\xf0\x9f\x98\x80", 6));
text = scr_bigint_to_string(scr_dyn_obj_get(root, "integer", 7)->v.bigint, 10);
assert(text->len == 30 && !memcmp(text->data, "123456789012345678901234567890", 30));
scr_str_release(text);
ScrDyn *array = scr_dyn_obj_get(root, "array", 5);
assert(array->v.arr.len == 4);
assert(!scr_dyn_arr_has_index(array, 0) && scr_dyn_arr_has_index(array, 1));
assert(!scr_dyn_arr_has_index(array, 2) && scr_dyn_arr_has_index(array, 3));
assert(array->v.arr.items[3] == first);
ScrDyn *bytes = scr_dyn_obj_get(root, "bytes", 5);
assert(bytes == scr_dyn_obj_get(root, "again", 5));
assert(bytes->v.bytes->elem == SCR_BYTES_U16 && bytes->v.bytes->len == 3);
assert(scr_bytes_byte_offset(bytes->v.bytes) == 4);
ScrBytes *buffer = scr_dyn_obj_get(root, "buffer", 6)->v.handle.ptr;
assert(bytes->v.bytes->backing == buffer && buffer->len == 16);
bytes->v.bytes->data[0] = 91;
assert(buffer->data[4] == 91);
scr_dyn_release(root);
scr_context_cleanup();
return NULL;
}
static SCR_TL char getters[4];
static SCR_TL size_t getter_count;
static ScrDyn *read_getter(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)args; (void)argc;
double value = scr_box_get_f64(closure->caps[0]);
getters[getter_count++] = (char)value;
return scr_dyn_new_num(value);
}
static void define_getter(ScrDyn *object, const char *key, char marker) {
ScrClosure *closure = scr_closure_new(NULL, 1);
closure->caps[0] = scr_box_new(SCR_BOX_F64);
scr_box_set_f64(closure->caps[0], marker);
ScrDyn *descriptor = scr_dyn_new_obj();
scr_dyn_obj_set(descriptor, "get", 3, scr_dyn_new_func(closure, read_getter, 0, "", ""));
scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true));
ScrStr *name = scr_str_new(key, strlen(key));
ScrDyn *name_value = scr_dyn_new_str(name);
ScrDyn *result = scr_dyn_define_property(object, name_value, descriptor);
assert(result && !scr_exc_pending());
scr_dyn_release(result);
scr_dyn_release(name_value);
scr_dyn_release(descriptor);
scr_str_release(name);
}
static void check_getters(void) {
ScrDyn *root = scr_dyn_new_obj();
ScrDyn *nested = scr_dyn_new_obj();
define_getter(nested, "inner", 'a');
scr_dyn_obj_set(root, "first", 5, nested);
define_getter(root, "last", 'b');
ScrMessage *message = scr_message_encode(root);
assert(message && !scr_exc_pending());
assert(getter_count == 2 && !memcmp(getters, "ab", 2));
scr_message_free(message);
scr_dyn_release(root);
scr_collect_cycles();
}
static void check_rejection(void) {
ScrDyn *root = scr_dyn_new_obj();
ScrClosure *closure = scr_closure_new(NULL, 0);
ScrDyn *function = scr_dyn_new_func(closure, read_getter, 0, "", "");
scr_dyn_obj_set(root, "function", 8, function);
assert(!scr_message_encode(root) && scr_exc_pending());
scr_exc_clear();
scr_dyn_release(root);
scr_collect_cycles();
}
/* Plain JSON-shaped documents decode as an intact region (ScrDynRegion);
* a later back edge clears it, so the cycle is still collected. */
static void check_documents(void) {
ScrDyn *root = scr_dyn_new_obj();
ScrDyn *child = scr_dyn_new_obj();
ScrDyn *list = scr_dyn_new_arr();
scr_dyn_arr_push(list, scr_dyn_new_num(1));
ScrStr *text = scr_str_new("x", 1);
scr_dyn_arr_push(list, scr_dyn_new_str(text));
scr_str_release(text);
scr_dyn_obj_set(child, "list", 4, list);
scr_dyn_obj_set(root, "child", 5, child);
scr_dyn_obj_set(root, "none", 4, scr_dyn_new_null());
ScrMessage *message = scr_message_encode(root);
assert(message && !scr_exc_pending());
scr_dyn_release(root);
ScrDyn *decoded = scr_message_decode(message);
assert(decoded && !scr_exc_pending());
ScrDyn *decoded_child = scr_dyn_obj_get(decoded, "child", 5);
const ScrDynRegion *region = scr_dyn_region_of(decoded);
assert(region && region->intact);
assert(scr_dyn_region_of(decoded_child) == region);
assert(scr_dyn_region_of(scr_dyn_obj_get(decoded_child, "list", 4)) == region);
scr_dyn_obj_set(decoded_child, "parent", 6, scr_dyn_retain(decoded));
assert(!region->intact);
scr_dyn_release(decoded);
scr_collect_cycles();
scr_message_free(message);
/* Shared subtrees and cycles (make_graph) stay untagged. */
ScrDyn *graph = make_graph();
message = scr_message_encode(graph);
scr_dyn_release(graph);
decoded = scr_message_decode(message);
assert(decoded && !scr_dyn_region_of(decoded));
scr_dyn_release(decoded);
scr_collect_cycles();
scr_message_free(message);
}
/* A link-gated unit's handle crosses threads through the transfer hooks in
* its ops: share on the sender, adopt on the receiver, drop with the
* message. The test borrows the last capacity slot, which no unit uses, so
* it also proves that slots past the tags in use work. */
#define TRANSFER_TAG ((ScrDynHandleTag)(SCR_DYNH_CAPACITY - 1))
typedef struct {
_Atomic size_t rc;
int value;
} TransferBox;
static _Atomic int transfer_shares, transfer_adopts, transfer_drops;
static void *transfer_retain(void *h) {
((TransferBox *)h)->rc++;
return h;
}
static void transfer_release(void *h) {
if (--((TransferBox *)h)->rc == 0) free(h);
}
static void *transfer_share(void *h) {
TransferBox *source = h;
if (source->value < 0) return NULL;
transfer_shares++;
TransferBox *copy = malloc(sizeof *copy);
copy->rc = 1;
copy->value = source->value;
return copy;
}
static ScrDyn *transfer_adopt(void *payload) {
transfer_adopts++;
return scr_dyn_new_handle(payload, TRANSFER_TAG);
}
static void transfer_drop(void *payload) {
transfer_drops++;
transfer_release(payload);
}
static const ScrDynHandleTransfer transfer_hooks = {transfer_share, transfer_adopt, transfer_drop};
static const ScrDynHandleOps transfer_ops = {
.cls = "Transferable",
.retain = transfer_retain,
.release = transfer_release,
.transfer = &transfer_hooks,
};
static const ScrDynHandleOps opaque_ops = {
.cls = "Opaque",
.retain = transfer_retain,
.release = transfer_release,
};
static void *receive_handles(void *data) {
scr_context_enter(1);
scr_init();
scr_lib_init(0, NULL);
scr_dyn_handle_install(TRANSFER_TAG, &transfer_ops);
ScrDyn *root = scr_message_decode(data);
assert(root && !scr_exc_pending());
ScrDyn *first = scr_dyn_obj_get(root, "first", 5);
assert(first->kind == SCR_DYN_HANDLE && first->v.handle.tag == TRANSFER_TAG);
assert(((TransferBox *)first->v.handle.ptr)->value == 7);
/* One handle reached twice decodes to one box. */
assert(first == scr_dyn_obj_get(root, "again", 5));
assert(transfer_adopts == 1);
scr_dyn_release(root);
scr_collect_cycles();
scr_context_cleanup();
return NULL;
}
static ScrDyn *transfer_handle(int value, const ScrDynHandleOps *ops) {
scr_dyn_handle_install(TRANSFER_TAG, ops);
TransferBox *box = malloc(sizeof *box);
box->rc = 1;
box->value = value;
ScrDyn *handle = scr_dyn_new_handle(box, TRANSFER_TAG);
transfer_release(box);
return handle;
}
static void check_handle_transfer(void) {
ScrDyn *root = scr_dyn_new_obj();
ScrDyn *handle = transfer_handle(7, &transfer_ops);
scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(handle));
scr_dyn_obj_set(root, "again", 5, handle);
ScrMessage *message = scr_message_encode(root);
assert(message && !scr_exc_pending());
assert(transfer_shares == 1);
scr_dyn_release(root);
pthread_t receiver;
assert(pthread_create(&receiver, NULL, receive_handles, message) == 0);
assert(pthread_join(receiver, NULL) == 0);
assert(transfer_drops == 0);
scr_message_free(message);
assert(transfer_drops == 1);
/* A handle whose unit declines (share answers NULL) and a handle with no
* transfer hooks both throw DataCloneError, releasing what was shared. */
root = scr_dyn_new_arr();
scr_dyn_arr_push(root, transfer_handle(1, &transfer_ops));
scr_dyn_arr_push(root, transfer_handle(-1, &transfer_ops));
assert(!scr_message_encode(root) && scr_exc_pending());
scr_exc_clear();
assert(transfer_shares == 2 && transfer_drops == 2);
scr_dyn_release(root);
root = transfer_handle(3, &opaque_ops);
assert(!scr_message_encode(root) && scr_exc_pending());
scr_exc_clear();
assert(transfer_shares == 2);
scr_dyn_release(root);
scr_collect_cycles();
}
int main(void) {
scr_init();
scr_lib_init(0, NULL);
ScrDyn *source = make_graph();
ScrMessage *message = scr_message_encode(source);
assert(message && !scr_exc_pending());
scr_dyn_release(source);
/* The sender's graph is gone before any recipient starts decoding. */
scr_collect_cycles();
pthread_t receiver;
assert(pthread_create(&receiver, NULL, receive_graph, message) == 0);
assert(pthread_join(receiver, NULL) == 0);
scr_message_free(message);
check_getters();
check_rejection();
check_documents();
check_handle_transfer();
scr_context_cleanup();
puts("message graphs preserve identity, sparse values, byte aliases and getter order");
puts("native handles cross threads through their transfer hooks");
}