Merge branch 'json_gc_fix'

This commit is contained in:
2026-02-17 14:00:28 -06:00
3 changed files with 219 additions and 9 deletions

View File

@@ -5749,6 +5749,18 @@ exception:
return JS_EXCEPTION;
}
/* Check if val is already on the visited stack (circular reference detection).
Uses identity comparison (===) since we're checking for the same object. */
static BOOL json_stack_has (JSContext *ctx, JSValue stack, JSValue val) {
if (!JS_IsArray (stack)) return FALSE;
JSArray *arr = JS_VALUE_GET_ARRAY (stack);
for (word_t i = 0; i < arr->len; i++) {
if (JS_StrictEq (ctx, arr->values[i], val))
return TRUE;
}
return FALSE;
}
static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue holder, JSValue val, JSValue indent) {
JSValue v;
int64_t i, len;
@@ -5784,9 +5796,7 @@ static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue ho
if (mist_is_gc_object (
val_ref.val)) { /* includes arrays (OBJ_ARRAY) since they have JS_TAG_PTR */
v = js_array_includes (ctx, jsc->stack, 1, &val_ref.val);
if (JS_IsException (v)) goto exception;
if (JS_ToBool (ctx, v)) {
if (json_stack_has (ctx, jsc->stack, val_ref.val)) {
JS_ThrowTypeError (ctx, "circular reference");
goto exception;
}
@@ -5801,8 +5811,7 @@ static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue ho
sep_ref.val = jsc->empty;
sep1_ref.val = jsc->empty;
}
v = js_cell_push (ctx, jsc->stack, 1, &val_ref.val);
if (check_exception_free (ctx, v)) goto exception;
if (JS_ArrayPush (ctx, &jsc->stack, val_ref.val) < 0) goto exception;
ret = JS_IsArray (val_ref.val);
if (ret < 0) goto exception;
if (ret) {
@@ -5890,8 +5899,8 @@ static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue ho
}
JSC_B_PUTC (jsc, '}');
}
if (check_exception_free (ctx, js_cell_pop (ctx, jsc->stack, 0, NULL)))
goto exception;
v = JS_ArrayPop (ctx, jsc->stack);
if (JS_IsException (v)) goto exception;
goto done;
}
switch (JS_VALUE_GET_NORM_TAG (val_ref.val)) {

View File

@@ -1847,7 +1847,25 @@ TEST(is_integer_vs_number) {
/* JSON Tests */
TEST(json_encode_object) {
/* Skip - requires GC rooting fixes in JS_JSONStringify */
/* Build an object with several properties and stringify with pretty=true */
JSGCRef obj_ref;
JS_PushGCRef(ctx, &obj_ref);
obj_ref.val = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj_ref.val, "name", JS_NewString(ctx, "test"));
JS_SetPropertyStr(ctx, obj_ref.val, "value", JS_NewInt32(ctx, 42));
JS_SetPropertyStr(ctx, obj_ref.val, "active", JS_NewBool(ctx, 1));
JS_SetPropertyStr(ctx, obj_ref.val, "tag", JS_NewString(ctx, "hello world"));
JSValue space = JS_NewInt32(ctx, 2);
JSValue str = JS_JSONStringify(ctx, obj_ref.val, JS_NULL, space, 1);
JS_PopGCRef(ctx, &obj_ref);
ASSERT(!JS_IsException(str));
ASSERT(JS_IsText(str));
const char *s = JS_ToCString(ctx, str);
ASSERT(s != NULL);
ASSERT(strstr(s, "\"name\"") != NULL);
ASSERT(strstr(s, "\"test\"") != NULL);
ASSERT(strstr(s, "42") != NULL);
JS_FreeCString(ctx, s);
return 1;
}
@@ -1867,7 +1885,98 @@ TEST(json_decode_object) {
}
TEST(json_roundtrip_array) {
/* Skip - requires GC rooting fixes in JS_JSONStringify */
JSGCRef arr_ref;
JS_PushGCRef(ctx, &arr_ref);
arr_ref.val = JS_NewArray(ctx);
JS_ArrayPush(ctx, &arr_ref.val, JS_NewInt32(ctx, 10));
JS_ArrayPush(ctx, &arr_ref.val, JS_NewString(ctx, "two"));
JS_ArrayPush(ctx, &arr_ref.val, JS_NewBool(ctx, 0));
JSValue str = JS_JSONStringify(ctx, arr_ref.val, JS_NULL, JS_NULL, 0);
JS_PopGCRef(ctx, &arr_ref);
ASSERT(!JS_IsException(str));
ASSERT(JS_IsText(str));
const char *s = JS_ToCString(ctx, str);
ASSERT(s != NULL);
ASSERT(strstr(s, "10") != NULL);
ASSERT(strstr(s, "\"two\"") != NULL);
ASSERT(strstr(s, "false") != NULL);
JS_FreeCString(ctx, s);
return 1;
}
TEST(json_encode_large_object) {
/* Stress test: build object with many properties, stringify with pretty print.
Under FORCE_GC_AT_MALLOC this will trigger GC on every allocation,
exposing any GC rooting bugs in the JSON encoder. */
JSGCRef obj_ref, str_ref;
JS_PushGCRef(ctx, &obj_ref);
JS_PushGCRef(ctx, &str_ref);
obj_ref.val = JS_NewObject(ctx);
char key[32], val[64];
for (int i = 0; i < 50; i++) {
snprintf(key, sizeof(key), "key_%d", i);
snprintf(val, sizeof(val), "value_%d_with_some_padding_text", i);
JS_SetPropertyStr(ctx, obj_ref.val, key, JS_NewString(ctx, val));
}
JSValue space = JS_NewInt32(ctx, 2);
str_ref.val = JS_JSONStringify(ctx, obj_ref.val, JS_NULL, space, 1);
ASSERT(!JS_IsException(str_ref.val));
ASSERT(JS_IsText(str_ref.val));
const char *s = JS_ToCString(ctx, str_ref.val);
ASSERT(s != NULL);
ASSERT(strstr(s, "\"key_0\"") != NULL);
ASSERT(strstr(s, "\"key_49\"") != NULL);
JS_FreeCString(ctx, s);
JS_PopGCRef(ctx, &str_ref);
JS_PopGCRef(ctx, &obj_ref);
return 1;
}
TEST(json_encode_nested) {
/* Nested objects stress test */
JSGCRef outer_ref, inner_ref, str_ref;
JS_PushGCRef(ctx, &outer_ref);
JS_PushGCRef(ctx, &inner_ref);
JS_PushGCRef(ctx, &str_ref);
outer_ref.val = JS_NewObject(ctx);
char key[32], val[64];
for (int i = 0; i < 20; i++) {
inner_ref.val = JS_NewObject(ctx);
for (int j = 0; j < 10; j++) {
snprintf(key, sizeof(key), "f%d", j);
snprintf(val, sizeof(val), "v_%d_%d", i, j);
JS_SetPropertyStr(ctx, inner_ref.val, key, JS_NewString(ctx, val));
}
snprintf(key, sizeof(key), "obj_%d", i);
JS_SetPropertyStr(ctx, outer_ref.val, key, inner_ref.val);
}
JSValue space = JS_NewInt32(ctx, 2);
str_ref.val = JS_JSONStringify(ctx, outer_ref.val, JS_NULL, space, 1);
ASSERT(!JS_IsException(str_ref.val));
ASSERT(JS_IsText(str_ref.val));
const char *s = JS_ToCString(ctx, str_ref.val);
ASSERT(s != NULL);
ASSERT(strstr(s, "\"obj_0\"") != NULL);
ASSERT(strstr(s, "\"f0\"") != NULL);
JS_FreeCString(ctx, s);
JS_PopGCRef(ctx, &str_ref);
JS_PopGCRef(ctx, &inner_ref);
JS_PopGCRef(ctx, &outer_ref);
return 1;
}
TEST(json_circular_reference) {
/* Circular reference should throw, not infinite recurse */
JSGCRef obj_ref;
JS_PushGCRef(ctx, &obj_ref);
obj_ref.val = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj_ref.val, "name", JS_NewString(ctx, "root"));
/* Create circular reference: obj.self = obj */
JS_SetPropertyStr(ctx, obj_ref.val, "self", obj_ref.val);
JSValue str = JS_JSONStringify(ctx, obj_ref.val, JS_NULL, JS_NULL, 0);
JS_PopGCRef(ctx, &obj_ref);
/* Should be an exception (circular reference), not a crash */
ASSERT(JS_IsException(str));
return 1;
}
@@ -2196,6 +2305,9 @@ int run_c_test_suite(JSContext *ctx)
RUN_TEST(json_encode_object);
RUN_TEST(json_decode_object);
RUN_TEST(json_roundtrip_array);
RUN_TEST(json_encode_large_object);
RUN_TEST(json_encode_nested);
RUN_TEST(json_circular_reference);
printf("\nSerialization - NOTA:\n");
RUN_TEST(nota_encode_int);

View File

@@ -5011,6 +5011,95 @@ run("nested function used after definition", function() {
assert_eq(result[1], 'b = "hi"', "nested fn encode text")
})
// ============================================================================
// JSON ENCODING
// ============================================================================
def json = use("json")
run("json encode flat object", function() {
var obj = {}
var i = 0
for (i = 0; i < 500; i++) {
obj[text(i)] = "value_" + text(i)
}
var result = json.encode(obj)
assert_eq(is_text(result), true, "encode returns text")
var decoded = json.decode(result)
assert_eq(decoded["0"], "value_0", "first property survives roundtrip")
assert_eq(decoded["499"], "value_499", "last property survives roundtrip")
})
run("json encode nested objects", function() {
var outer = {}
var i = 0
var j = 0
var inner = null
for (i = 0; i < 50; i++) {
inner = {}
for (j = 0; j < 20; j++) {
inner[text(j)] = i * 20 + j
}
outer[text(i)] = inner
}
var result = json.encode(outer)
var decoded = json.decode(result)
assert_eq(decoded["0"]["0"], 0, "nested first value")
assert_eq(decoded["49"]["19"], 999, "nested last value")
})
run("json encode array", function() {
var arr = [1, "two", true, null, 3.14]
var result = json.encode(arr)
var decoded = json.decode(result)
assert_eq(decoded[0], 1, "array number")
assert_eq(decoded[1], "two", "array text")
assert_eq(decoded[2], true, "array logical")
assert_eq(decoded[3], null, "array null")
assert_eq(decoded[4], 3.14, "array float")
})
run("json circular reference detected", function() {
var circ = {}
circ.name = "root"
circ.self = circ
if (!should_disrupt(function() { json.encode(circ) })) {
fail("circular reference not detected")
}
})
run("json deeply nested circular reference", function() {
var a = {}
var b = {}
var c = {}
a.child = b
b.child = c
c.child = a
if (!should_disrupt(function() { json.encode(a) })) {
fail("deep circular reference not detected")
}
})
run("json roundtrip preserves types", function() {
var obj = {
"num": 42,
"txt": "hello",
"yes": true,
"no": false,
"nil": null,
"arr": [1, 2, 3],
"sub": {"a": 1}
}
var decoded = json.decode(json.encode(obj))
assert_eq(decoded.num, 42, "number preserved")
assert_eq(decoded.txt, "hello", "text preserved")
assert_eq(decoded.yes, true, "true preserved")
assert_eq(decoded.no, false, "false preserved")
assert_eq(decoded.nil, null, "null preserved")
assert_eq(decoded.arr[2], 3, "array preserved")
assert_eq(decoded.sub.a, 1, "sub-object preserved")
})
// ============================================================================
// SUMMARY
// ============================================================================