From 51815b66d84be2bb64f16b174add27c69b4fcc00 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 17 Feb 2026 14:00:23 -0600 Subject: [PATCH] json rooting fix --- source/runtime.c | 23 +++++++--- source/suite.c | 116 ++++++++++++++++++++++++++++++++++++++++++++++- vm_suite.ce | 89 ++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 9 deletions(-) diff --git a/source/runtime.c b/source/runtime.c index 9313ad6b..08be4ee9 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -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)) { diff --git a/source/suite.c b/source/suite.c index a64385ab..bfb791c6 100644 --- a/source/suite.c +++ b/source/suite.c @@ -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); diff --git a/vm_suite.ce b/vm_suite.ce index ec6c566f..c14dd11c 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -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 // ============================================================================