mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-04 17:41:42 +08:00
- Preserve fresh constructor identity and shared captures across local class instances. - Carry constructors through unknown values without a JavaScript engine. - Support runtime-length spreads into fixed and rest parameters.
542 lines
22 KiB
C
542 lines
22 KiB
C
/* Oracle test for scr_json.c (the dynamic-value dyn, the RFC 8259 parser,
|
|
* the dynCheck failure path, and the stringify output buffer). Run by
|
|
* json.test.ts; built with ASan + the RC audit, so a clean exit also proves
|
|
* the checked-dynamic tree's recursive ownership (parse failures mid-tree included) leaks
|
|
* nothing and frees nothing twice.
|
|
*
|
|
* EXACT ERROR MESSAGES are asserted here, against OUR strings: compiled
|
|
* programs cannot observe them (the supported catch form is bindingless),
|
|
* and the V8-flavored parse messages are documented as approximate
|
|
* (SEMANTICS.md) — so the C tests, not the differential corpus, are where
|
|
* the exact texts are pinned. Each expected failure routes through
|
|
* scr_exc_print_uncaught(), and json.test.ts asserts the stderr lines.
|
|
*/
|
|
#include "scr_runtime.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static int checks = 0;
|
|
static int failures = 0;
|
|
|
|
static void check(bool ok, const char *name) {
|
|
checks++;
|
|
if (!ok) {
|
|
failures++;
|
|
printf("FAIL %s\n", name);
|
|
}
|
|
}
|
|
|
|
static ScrStr *S(const char *s) { return scr_str_new(s, strlen(s)); }
|
|
|
|
static bool str_is(const ScrStr *s, const char *want) {
|
|
return s && s->len == strlen(want) && memcmp(s->data, want, s->len) == 0;
|
|
}
|
|
|
|
static bool typed_view_fails = false;
|
|
static void *view_retain(void *value) { return scr_str_retain(value); }
|
|
static void view_release(void *value) { scr_str_release(value); }
|
|
static ScrDyn *view_materialize(void *value) {
|
|
if (typed_view_fails) return scr_dyn_class_view_unavailable(value);
|
|
ScrDyn *view = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(view, "value", 5, scr_dyn_new_num(7));
|
|
return view;
|
|
}
|
|
|
|
static void typed_view_failure_tests(void) {
|
|
ScrStr *source = S("native identity");
|
|
ScrDyn *capsule = scr_dyn_new_typed_ref(source, view_retain, view_release,
|
|
"test", 4, view_materialize, NULL);
|
|
ScrDyn *first = scr_dyn_typed_ref_materialize(capsule);
|
|
check(first->kind == SCR_DYN_OBJ && !scr_exc_pending(), "typed view begins valid");
|
|
for (int i = 0; i < 3; i++) {
|
|
typed_view_fails = true;
|
|
ScrDyn *failed = scr_dyn_typed_ref_materialize(capsule);
|
|
check(failed->kind == SCR_DYN_UNDEF && scr_exc_pending(), "typed view failure stays catchable");
|
|
scr_exc_clear();
|
|
scr_dyn_release(failed);
|
|
check(first->kind == SCR_DYN_OBJ, "failed refresh preserves cached view");
|
|
typed_view_fails = false;
|
|
ScrDyn *next = scr_dyn_typed_ref_materialize(capsule);
|
|
check(next == first && !scr_exc_pending(), "typed view recovers with stable identity");
|
|
scr_dyn_release(next);
|
|
ScrStr *roundtrip = scr_dyn_typed_ref_unbox(capsule);
|
|
check(roundtrip == source, "failed view preserves native identity");
|
|
scr_str_release(roundtrip);
|
|
}
|
|
scr_dyn_release(first);
|
|
scr_dyn_release(capsule);
|
|
scr_str_release(source);
|
|
}
|
|
|
|
/* Parse `text`, expect success, return the checked-dynamic tree (+1). */
|
|
static ScrDyn *parse_ok(const char *text, const char *name) {
|
|
ScrStr *t = S(text);
|
|
ScrDyn *d = scr_json_parse(t);
|
|
scr_str_release(t);
|
|
check(d != NULL && !scr_exc_pending(), name);
|
|
if (scr_exc_pending()) scr_exc_clear();
|
|
return d;
|
|
}
|
|
|
|
/* Parse `text`, expect a throw; the message prints to stderr (asserted by
|
|
* json.test.ts) which also clears the cell. */
|
|
static void parse_fail(const char *text, const char *name) {
|
|
ScrStr *t = S(text);
|
|
ScrDyn *d = scr_json_parse(t);
|
|
scr_str_release(t);
|
|
check(d == NULL && scr_exc_pending(), name);
|
|
if (d) scr_dyn_release(d);
|
|
if (scr_exc_pending()) scr_exc_print_uncaught();
|
|
}
|
|
|
|
static bool dyn_str_is(const ScrDyn *d, const char *want) {
|
|
return d && d->kind == SCR_DYN_STR && str_is(d->v.str, want);
|
|
}
|
|
|
|
/* Low-level callback contracts that typed source does not expose: cyclic
|
|
* dyn graphs, receiver lifetime during mutation, and runtime refusal paths.
|
|
* Every temporary remains under the existing ASan and RC audit harness. */
|
|
static size_t callback_calls;
|
|
static int callback_mode;
|
|
static ScrDyn *callback_shared;
|
|
|
|
static ScrDyn *json_test_callback(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
|
(void)closure;
|
|
callback_calls++;
|
|
check(argc == 2, "JSON callback receives key and value");
|
|
check(args[0]->kind == SCR_DYN_STR, "JSON callback key is a string");
|
|
ScrDyn *holder = scr_dyn_this_get();
|
|
check(holder->kind == SCR_DYN_OBJ || holder->kind == SCR_DYN_ARR || holder->kind == SCR_DYN_BYTES, "JSON callback holder bound");
|
|
const ScrStr *key = args[0]->v.str;
|
|
ScrDyn *value = args[1];
|
|
if (callback_mode == 7 && (str_is(key, "0") || str_is(key, "1"))) {
|
|
check(holder == callback_shared, "returned bytes remain the holder for indexed callbacks");
|
|
}
|
|
if (callback_mode == 1 && key->len) {
|
|
scr_dyn_release(holder);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (callback_mode == 2 && str_is(key, "a")) {
|
|
/* Reallocating the holder's entries must not invalidate traversal. */
|
|
for (size_t i = 0; i < 80; i++) {
|
|
char name[32];
|
|
int length = snprintf(name, sizeof name, "new%zu", i);
|
|
scr_dyn_obj_set(holder, name, (size_t)length, scr_dyn_new_num((double)i));
|
|
}
|
|
scr_dyn_obj_set(holder, "b", 1, scr_dyn_new_num(9));
|
|
}
|
|
if (callback_mode == 3 && key->len) {
|
|
scr_throw_error_msg(SCR_ERR_TYPE, "callback", 8);
|
|
scr_dyn_release(holder);
|
|
return NULL;
|
|
}
|
|
if (callback_mode == 4 && str_is(key, "a")) {
|
|
/* Release the original parent edge while its value is borrowed by the
|
|
* callback. The walker must still own that original value. */
|
|
scr_dyn_obj_set(holder, "a", 1, scr_dyn_new_null());
|
|
check(value->kind == SCR_DYN_OBJ, "replaced child survives callback");
|
|
}
|
|
if (callback_mode == 5) {
|
|
ScrDyn *replacement = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(replacement, "child", 5, scr_dyn_new_null());
|
|
scr_dyn_release(holder);
|
|
return replacement;
|
|
}
|
|
if ((callback_mode == 6 && key->len) || (callback_mode == 7 && str_is(key, "replace"))) {
|
|
scr_dyn_release(holder);
|
|
return scr_dyn_retain(callback_shared);
|
|
}
|
|
scr_dyn_release(holder);
|
|
return scr_dyn_retain(value);
|
|
}
|
|
|
|
static ScrDyn *json_test_to_json(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
|
(void)closure;
|
|
check(argc == 1, "toJSON receives only the property key");
|
|
check(dyn_str_is(args[0], "nested"), "toJSON receives containing key");
|
|
ScrDyn *holder = scr_dyn_this_get();
|
|
check(holder == callback_shared, "toJSON receiver is the value");
|
|
/* Drop the only owning property reference to this callable while it is
|
|
* executing; the invocation must retain its closure independently. */
|
|
scr_dyn_obj_set(holder, "toJSON", 6, scr_dyn_new_null());
|
|
scr_dyn_release(holder);
|
|
return scr_dyn_new_num(42);
|
|
}
|
|
|
|
static ScrDyn *json_test_func(ScrDynThunk thunk) {
|
|
return scr_dyn_new_func(scr_closure_new(NULL, 0), thunk, 2, "JSON test callback", "json_test");
|
|
}
|
|
|
|
static ScrDyn *json_test_stringify(ScrDyn *value, ScrDyn *callback, const char *gap) {
|
|
ScrStr *indent = S(gap);
|
|
ScrDyn *out = scr_json_stringify_replacer(value, callback, indent);
|
|
scr_str_release(indent);
|
|
return out;
|
|
}
|
|
|
|
static ScrDyn *json_test_parse(const char *text, ScrDyn *callback) {
|
|
ScrStr *input = S(text);
|
|
ScrDyn *out = scr_json_parse_reviver(input, callback);
|
|
scr_str_release(input);
|
|
return out;
|
|
}
|
|
|
|
static void json_callback_tests(void) {
|
|
ScrDyn *callback = json_test_func(json_test_callback);
|
|
callback_mode = 0;
|
|
callback_calls = 0;
|
|
ScrDyn *value = parse_ok("{\"10\":10,\"2\":2,\"a\":[1,true,null]}", "callback input");
|
|
ScrDyn *out = json_test_stringify(value, callback, " ");
|
|
check(dyn_str_is(out, "{\n \"2\": 2,\n \"10\": 10,\n \"a\": [\n 1,\n true,\n null\n ]\n}"), "replacer pretty output");
|
|
check(callback_calls == 7, "replacer invokes once per property plus root");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(value);
|
|
|
|
callback_mode = 1;
|
|
value = parse_ok("{\"a\":1,\"b\":2}", "omission input");
|
|
out = json_test_stringify(value, callback, " ");
|
|
check(dyn_str_is(out, "{}"), "all omitted properties have no blank lines");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(value);
|
|
out = json_test_parse("{\"a\":1,\"b\":2}", callback);
|
|
check(out && out->kind == SCR_DYN_OBJ && out->v.obj.len == 0, "reviver deletes actual object properties");
|
|
scr_dyn_release(out);
|
|
out = json_test_parse("[1]", callback);
|
|
check(!out && scr_exc_pending(), "sparse reviver result refuses instead of returning a dense undefined");
|
|
scr_exc_clear();
|
|
|
|
callback_mode = 2;
|
|
callback_calls = 0;
|
|
value = parse_ok("{\"a\":1,\"b\":2}", "mutation input");
|
|
out = json_test_stringify(value, callback, "");
|
|
check(dyn_str_is(out, "{\"a\":1,\"b\":9}"), "replacer snapshots keys but reads current values");
|
|
check(callback_calls == 3, "replacer skips keys added during traversal");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(value);
|
|
callback_calls = 0;
|
|
out = json_test_parse("{\"a\":1,\"b\":2}", callback);
|
|
check(out && scr_dyn_obj_get(out, "b", 1)->v.num == 9, "reviver reads changed sibling");
|
|
check(out && out->v.obj.len == 82, "reviver retains new properties without visiting them");
|
|
check(callback_calls == 3, "reviver key snapshot survives realloc");
|
|
scr_dyn_release(out);
|
|
|
|
callback_mode = 4;
|
|
value = parse_ok("{\"a\":{\"x\":1}}", "parent overwrite input");
|
|
out = json_test_stringify(value, callback, "");
|
|
check(dyn_str_is(out, "{\"a\":{\"x\":1}}"), "replacer owns value after edge replacement");
|
|
check(scr_dyn_obj_get(value, "a", 1)->kind == SCR_DYN_NULL, "replacer mutation reaches holder");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(value);
|
|
out = json_test_parse("{\"a\":{\"x\":1}}", callback);
|
|
check(out && scr_dyn_obj_get(out, "a", 1)->kind == SCR_DYN_OBJ, "reviver returned value wins over holder assignment");
|
|
scr_dyn_release(out);
|
|
|
|
callback_mode = 0;
|
|
callback_shared = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(callback_shared, "toJSON", 6, json_test_func(json_test_to_json));
|
|
value = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(value, "nested", 6, scr_dyn_retain(callback_shared));
|
|
out = json_test_stringify(value, callback, "");
|
|
check(dyn_str_is(out, "{\"nested\":42}"), "toJSON precedes replacer and owns its callable");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(value);
|
|
scr_dyn_release(callback_shared);
|
|
callback_shared = NULL;
|
|
|
|
/* Shared subtrees are not cycles. A true back-edge throws only after
|
|
* the replacer gets its chance to replace that edge. Break it manually
|
|
* after the test: dyn graphs do not use the typed cycle collector. */
|
|
value = scr_dyn_new_obj();
|
|
ScrDyn *shared = parse_ok("{\"n\":1}", "shared subtree");
|
|
scr_dyn_obj_set(value, "a", 1, scr_dyn_retain(shared));
|
|
scr_dyn_obj_set(value, "b", 1, scr_dyn_retain(shared));
|
|
out = json_test_stringify(value, callback, "");
|
|
check(dyn_str_is(out, "{\"a\":{\"n\":1},\"b\":{\"n\":1}}"), "DAG is not circular");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(shared);
|
|
scr_dyn_release(value);
|
|
value = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(value, "self", 4, scr_dyn_retain(value));
|
|
callback_calls = 0;
|
|
out = json_test_stringify(value, callback, "");
|
|
check(!out && scr_exc_pending(), "cyclic replacer output throws");
|
|
check(callback_calls == 2, "replacer observes cyclic edge before cycle error");
|
|
scr_exc_clear();
|
|
callback_mode = 1;
|
|
out = json_test_stringify(value, callback, "");
|
|
check(dyn_str_is(out, "{}"), "replacer may remove a cycle");
|
|
scr_dyn_release(out);
|
|
scr_dyn_obj_set(value, "self", 4, scr_dyn_new_null());
|
|
scr_dyn_release(value);
|
|
|
|
callback_mode = 5;
|
|
value = scr_dyn_new_null();
|
|
out = json_test_stringify(value, callback, "");
|
|
check(!out && scr_exc_pending(), "ever-growing replacements hit a catchable depth limit");
|
|
scr_exc_clear();
|
|
scr_dyn_release(value);
|
|
|
|
callback_mode = 6;
|
|
callback_shared = scr_dyn_new_num(INFINITY);
|
|
out = json_test_parse("{\"n\":0}", callback);
|
|
check(out && scr_dyn_obj_get(out, "n", 1) == callback_shared, "reviver retains returned shared value");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(callback_shared);
|
|
callback_shared = NULL;
|
|
|
|
/* Branded buffers can enter the dyn tree from native stream callbacks.
|
|
* Their pre-replacer toJSON differs from a buffer returned BY a replacer. */
|
|
const uint8_t bytes[] = { 5, 6 };
|
|
ScrBytes *storage = scr_bytes_from_data(bytes, sizeof bytes);
|
|
callback_shared = scr_dyn_new_buffer(storage);
|
|
scr_bytes_release(storage);
|
|
callback_mode = 0;
|
|
value = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(value, "buffer", 6, scr_dyn_retain(callback_shared));
|
|
out = json_test_stringify(value, callback, "");
|
|
check(dyn_str_is(out, "{\"buffer\":{\"type\":\"Buffer\",\"data\":[5,6]}}"), "branded buffer toJSON precedes callback");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(value);
|
|
callback_mode = 7;
|
|
value = parse_ok("{\"replace\":0}", "buffer replacement input");
|
|
out = json_test_stringify(value, callback, "");
|
|
check(dyn_str_is(out, "{\"replace\":{\"0\":5,\"1\":6}}"), "returned buffer does not rerun toJSON");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(value);
|
|
scr_dyn_release(callback_shared);
|
|
callback_shared = NULL;
|
|
|
|
for (size_t i = 0; i < 100; i++) {
|
|
callback_mode = 3;
|
|
out = json_test_parse("{\"a\":[1,2],\"b\":3}", callback);
|
|
check(!out && scr_exc_pending(), "reviver exception abandons all walk frames");
|
|
scr_exc_clear();
|
|
value = parse_ok("{\"a\":[1,2],\"b\":3}", "throw input");
|
|
out = json_test_stringify(value, callback, " ");
|
|
check(!out && scr_exc_pending(), "replacer exception abandons output and walk frames");
|
|
scr_exc_clear();
|
|
scr_dyn_release(value);
|
|
ScrDyn *receiver = scr_dyn_this_get();
|
|
check(receiver->kind == SCR_DYN_UNDEF, "receiver stack restored after exception");
|
|
scr_dyn_release(receiver);
|
|
}
|
|
callback_mode = 0;
|
|
callback_calls = 0;
|
|
out = json_test_parse("{\"a\":1,}", callback);
|
|
check(!out && scr_exc_pending() && callback_calls == 0, "parse error never runs reviver");
|
|
scr_exc_clear();
|
|
out = json_test_parse("17", callback);
|
|
check(out && out->kind == SCR_DYN_NUM && out->v.num == 17, "callback recovery after parse error");
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(callback);
|
|
}
|
|
|
|
static void test_class_values(void) {
|
|
ScrStr *name = S("Stored");
|
|
ScrClassObj template = { .rc = SIZE_MAX, .name = name, .length = 2 };
|
|
ScrClassObj *first = scr_classobj_new(&template, 1);
|
|
first->caps[0] = scr_box_new(SCR_BOX_F64);
|
|
scr_box_set_f64(first->caps[0], 42);
|
|
ScrClassObj *second = scr_classobj_new(&template, 0);
|
|
ScrDyn *a = scr_dyn_new_class(first, "classval:Stored");
|
|
ScrDyn *again = scr_dyn_new_class(first, "classval:Stored");
|
|
ScrDyn *b = scr_dyn_new_class(second, "classval:Stored");
|
|
scr_classobj_release(first);
|
|
scr_classobj_release(second);
|
|
check(scr_dyn_strict_eq(a, again) && !scr_dyn_strict_eq(a, b), "boxed class identity belongs to evaluation");
|
|
ScrClassObj *restored = scr_dyn_class_check(a, "classval:Stored", NULL);
|
|
check(restored && scr_box_get_f64(restored->caps[0]) == 42, "boxed class retains captured environment");
|
|
scr_classobj_release(restored);
|
|
check(!scr_dyn_class_check(a, "classval:Other", NULL) && scr_exc_pending(), "class casts check native constructor type");
|
|
scr_exc_clear();
|
|
ScrDyn *arity = scr_dyn_fn_get(a, "length", 6);
|
|
check(arity && arity->kind == SCR_DYN_NUM && arity->v.num == 2, "boxed constructor length");
|
|
scr_dyn_release(arity);
|
|
check(!scr_dyn_call(a, NULL, 0, "Stored") && scr_exc_pending(), "class call requires new");
|
|
scr_exc_clear();
|
|
check(!scr_dyn_obj_keys(a) && scr_exc_pending(), "class reflection refuses an incomplete property view");
|
|
scr_exc_clear();
|
|
scr_dyn_release(a);
|
|
scr_dyn_release(again);
|
|
scr_dyn_release(b);
|
|
scr_str_release(name);
|
|
}
|
|
|
|
int main(void) {
|
|
test_class_values();
|
|
scr_init();
|
|
|
|
/* ── primitives ─────────────────────────────────────────────────── */
|
|
ScrDyn *d = parse_ok("null", "parse null");
|
|
check(d->kind == SCR_DYN_NULL, "null kind");
|
|
scr_dyn_release(d);
|
|
|
|
d = parse_ok("true", "parse true");
|
|
check(d->kind == SCR_DYN_BOOL && d->v.b, "true value");
|
|
scr_dyn_release(d);
|
|
d = parse_ok("false", "parse false");
|
|
check(d->kind == SCR_DYN_BOOL && !d->v.b, "false value");
|
|
scr_dyn_release(d);
|
|
|
|
d = parse_ok(" -12.5e2 ", "parse number with ws");
|
|
check(d->kind == SCR_DYN_NUM && d->v.num == -1250.0, "number value");
|
|
scr_dyn_release(d);
|
|
d = parse_ok("0", "parse zero");
|
|
check(d->kind == SCR_DYN_NUM && d->v.num == 0, "zero value");
|
|
scr_dyn_release(d);
|
|
d = parse_ok("1e308", "parse big");
|
|
check(d->kind == SCR_DYN_NUM && d->v.num == 1e308, "big value");
|
|
scr_dyn_release(d);
|
|
d = parse_ok("1e999", "parse overflow to Infinity"); /* like JS */
|
|
check(d->kind == SCR_DYN_NUM && isinf(d->v.num), "overflow is Infinity");
|
|
scr_dyn_release(d);
|
|
|
|
/* ── strings and escapes ────────────────────────────────────────── */
|
|
d = parse_ok("\"a\\\"b\\\\c\\/d\\b\\f\\n\\r\\t\"", "parse escapes");
|
|
check(dyn_str_is(d, "a\"b\\c/d\b\f\n\r\t"), "escape values");
|
|
scr_dyn_release(d);
|
|
|
|
d = parse_ok("\"\\u0041\\u00e9\\u65e5\"", "parse \\u BMP");
|
|
check(dyn_str_is(d, "A\xC3\xA9\xE6\x97\xA5"), "\\u BMP encodes UTF-8");
|
|
scr_dyn_release(d);
|
|
|
|
d = parse_ok("\"\\uD83D\\uDE00\"", "parse surrogate pair");
|
|
check(dyn_str_is(d, "\xF0\x9F\x98\x80"), "surrogate pair combines");
|
|
scr_dyn_release(d);
|
|
|
|
/* Lone surrogates become U+FFFD (house policy: strings stay well-formed
|
|
* UTF-8; JS would keep the lone surrogate — documented divergence). */
|
|
d = parse_ok("\"x\\uD800y\"", "parse lone high surrogate");
|
|
check(dyn_str_is(d, "x\xEF\xBF\xBDy"), "lone high surrogate -> U+FFFD");
|
|
scr_dyn_release(d);
|
|
d = parse_ok("\"\\uDC00\"", "parse lone low surrogate");
|
|
check(dyn_str_is(d, "\xEF\xBF\xBD"), "lone low surrogate -> U+FFFD");
|
|
scr_dyn_release(d);
|
|
/* High surrogate followed by a non-surrogate escape: U+FFFD, then the
|
|
* escape parses normally. */
|
|
d = parse_ok("\"\\uD800\\u0041\"", "high surrogate then BMP escape");
|
|
check(dyn_str_is(d, "\xEF\xBF\xBD" "A"), "U+FFFD then A");
|
|
scr_dyn_release(d);
|
|
|
|
/* Raw UTF-8 passes through. */
|
|
d = parse_ok("\"caf\xC3\xA9\"", "parse raw UTF-8");
|
|
check(dyn_str_is(d, "caf\xC3\xA9"), "raw UTF-8 preserved");
|
|
scr_dyn_release(d);
|
|
|
|
/* ── arrays and objects ─────────────────────────────────────────── */
|
|
d = parse_ok("[1, [2, []], \"three\"]", "parse nested array");
|
|
check(d->kind == SCR_DYN_ARR && d->v.arr.len == 3, "array len");
|
|
check(d->v.arr.items[0]->v.num == 1, "arr[0]");
|
|
check(d->v.arr.items[1]->kind == SCR_DYN_ARR && d->v.arr.items[1]->v.arr.len == 2,
|
|
"arr[1] nested");
|
|
check(dyn_str_is(d->v.arr.items[2], "three"), "arr[2] string");
|
|
scr_dyn_release(d);
|
|
|
|
d = parse_ok("{\"a\":1,\"b\":{\"c\":[true]},\"a\":2}", "parse object");
|
|
check(d->kind == SCR_DYN_OBJ && d->v.obj.len == 2, "dup keys collapse");
|
|
const ScrDyn *m = scr_dyn_obj_get(d, "a", 1);
|
|
check(m != NULL && m->kind == SCR_DYN_NUM && m->v.num == 2, "later dup key wins");
|
|
m = scr_dyn_obj_get(d, "b", 1);
|
|
check(m != NULL && m->kind == SCR_DYN_OBJ, "nested object");
|
|
check(scr_dyn_obj_get(d, "zz", 2) == NULL, "absent key is NULL");
|
|
scr_dyn_release(d);
|
|
|
|
d = parse_ok("{}", "parse empty object");
|
|
check(d->v.obj.len == 0, "empty object");
|
|
scr_dyn_release(d);
|
|
d = parse_ok("[]", "parse empty array");
|
|
check(d->v.arr.len == 0, "empty array");
|
|
scr_dyn_release(d);
|
|
|
|
/* ── retain/release semantics ───────────────────────────────────── */
|
|
d = parse_ok("[1,2]", "rc probe");
|
|
ScrDyn *alias = scr_dyn_retain(d);
|
|
scr_dyn_release(d);
|
|
check(alias->v.arr.len == 2, "alias survives first release");
|
|
scr_dyn_release(alias);
|
|
|
|
/* ── parse errors (exact OUR-string messages, printed to stderr) ── */
|
|
parse_fail("", "empty input fails");
|
|
parse_fail(" ", "blank input fails");
|
|
parse_fail("{oops", "bad token fails");
|
|
parse_fail("[1,2,", "unterminated array fails");
|
|
parse_fail("[1 2]", "missing comma fails");
|
|
parse_fail("{\"a\"}", "missing colon fails");
|
|
parse_fail("{\"a\":1,}", "trailing comma fails");
|
|
parse_fail("{1:2}", "non-string key fails");
|
|
parse_fail("\"unterminated", "unterminated string fails");
|
|
parse_fail("\"bad \\q\"", "bad escape fails");
|
|
parse_fail("\"ctrl \n\"", "control char in string fails");
|
|
parse_fail("\"\\u12G4\"", "bad unicode escape fails");
|
|
parse_fail("-", "lone minus fails");
|
|
parse_fail("1.", "bare fraction fails");
|
|
parse_fail("1e+", "empty exponent fails");
|
|
parse_fail("1 2", "trailing content fails");
|
|
parse_fail("nul", "truncated literal fails");
|
|
|
|
/* Depth cap: 1001 nested arrays throw (catchably) instead of smashing
|
|
* the native stack. */
|
|
{
|
|
size_t n = 1001;
|
|
char *deep = malloc(2 * n + 2);
|
|
for (size_t i = 0; i < n; i++) deep[i] = '[';
|
|
deep[n] = '1';
|
|
for (size_t i = 0; i < n; i++) deep[n + 1 + i] = ']';
|
|
deep[2 * n + 1] = '\0';
|
|
ScrStr *t = scr_str_new(deep, 2 * n + 1);
|
|
free(deep);
|
|
ScrDyn *too = scr_json_parse(t);
|
|
scr_str_release(t);
|
|
check(too == NULL && scr_exc_pending(), "depth cap throws");
|
|
if (scr_exc_pending()) scr_exc_print_uncaught();
|
|
}
|
|
|
|
/* ── dynCheck failure messages (the emitted builders' shared path) ── */
|
|
{
|
|
ScrDyn *got = parse_ok("\"nope\"", "fail-path operand");
|
|
ScrDynPath root = { NULL, "items", 0 };
|
|
ScrDynPath idx = { &root, NULL, 2 };
|
|
ScrDynPath leaf = { &idx, "price", 0 };
|
|
scr_dyn_check_fail(&leaf, "number", got);
|
|
check(scr_exc_pending(), "check_fail throws");
|
|
scr_exc_print_uncaught(); /* "TypeError: expected number at $.items[2].price, got string" */
|
|
scr_dyn_check_fail(NULL, "object", NULL);
|
|
scr_exc_print_uncaught(); /* "TypeError: expected object at $, got undefined" */
|
|
scr_dyn_release(got);
|
|
}
|
|
|
|
/* ── stringify buffer ───────────────────────────────────────────── */
|
|
{
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_putc(&b, '[');
|
|
scr_jb_put_f64(&b, 0.0 / 0.0);
|
|
scr_jb_putc(&b, ',');
|
|
scr_jb_put_f64(&b, 1.0 / 0.0);
|
|
scr_jb_putc(&b, ',');
|
|
scr_jb_put_f64(&b, -0.0);
|
|
scr_jb_putc(&b, ',');
|
|
scr_jb_put_f64(&b, 0.1 + 0.2);
|
|
scr_jb_putc(&b, ',');
|
|
ScrStr *tricky = S("q\" b\\ n\n t\t ctl\x01 caf\xC3\xA9");
|
|
scr_jb_put_json_str(&b, tricky);
|
|
scr_str_release(tricky);
|
|
scr_jb_putc(&b, ']');
|
|
ScrStr *out = scr_jb_finish(&b);
|
|
check(str_is(out,
|
|
"[null,null,0,0.30000000000000004,"
|
|
"\"q\\\" b\\\\ n\\n t\\t ctl\\u0001 caf\xC3\xA9\"]"),
|
|
"stringify buffer output");
|
|
scr_str_release(out);
|
|
}
|
|
|
|
json_callback_tests();
|
|
typed_view_failure_tests();
|
|
|
|
printf("%d/%d checks passed\n", checks - failures, checks);
|
|
return failures == 0 ? 0 : 1;
|
|
}
|