From 3050a4419cdb886e2f4d7c4807f1226d2f477a63 Mon Sep 17 00:00:00 2001 From: ngallodev Date: Tue, 29 Sep 2026 08:11:59 +0000 Subject: [PATCH] fix(pipeline): decode persisted LSP arrays linearly --- src/pipeline/lsp_surface.c | 19 +++++++++++++------ tests/test_pipeline.c | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/pipeline/lsp_surface.c b/src/pipeline/lsp_surface.c index 369f7f7e57..aa7d5292e2 100644 --- a/src/pipeline/lsp_surface.c +++ b/src/pipeline/lsp_surface.c @@ -198,9 +198,13 @@ static const char **arena_str_array(CBMArena *arena, yyjson_val *arr, bool null_ if (!items) { return NULL; } - for (int i = 0; i < count; i++) { - const char *s = yyjson_get_str(yyjson_arr_get(arr, (size_t)i)); - items[i] = s ? cbm_arena_strdup(arena, s) : "?"; + /* Indexed access rescans non-flat arrays; iterate once to keep this O(n). */ + yyjson_arr_iter iter = yyjson_arr_iter_with(arr); + int i = 0; + yyjson_val *item; + while ((item = yyjson_arr_iter_next(&iter))) { + const char *s = yyjson_get_str(item); + items[i++] = s ? cbm_arena_strdup(arena, s) : "?"; } if (null_terminated) { items[count] = NULL; @@ -236,9 +240,12 @@ int cbm_lsp_surface_defs_from_json(CBMArena *arena, const char *defs_json, CBMLS return -1; } memset(defs, 0, (size_t)count * sizeof(CBMLSPDef)); - for (int i = 0; i < count; i++) { - yyjson_val *o = yyjson_arr_get(lsp, (size_t)i); - CBMLSPDef *d = &defs[i]; + /* `lsp` contains objects, so yyjson_arr_get(i) makes this decode O(n^2). */ + yyjson_arr_iter iter = yyjson_arr_iter_with(lsp); + int i = 0; + yyjson_val *o; + while ((o = yyjson_arr_iter_next(&iter))) { + CBMLSPDef *d = &defs[i++]; d->qualified_name = arena_str_or_null(arena, yyjson_obj_get(o, "qn")); d->short_name = arena_str_or_null(arena, yyjson_obj_get(o, "sn")); d->label = arena_str_or_null(arena, yyjson_obj_get(o, "lb")); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 17f37bd4d4..9c830d8f4e 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -9,7 +9,9 @@ #include "test_framework.h" #include "test_helpers.h" #include "foundation/mem.h" // cbm_mem_init/budget (back-pressure futile-nap test) +#include "foundation/arena.h" #include "pipeline/pipeline.h" +#include "pipeline/lsp_surface.h" #include "pipeline/pipeline_internal.h" #include "pipeline/artifact.h" #include "store/store.h" @@ -13008,6 +13010,31 @@ TEST(pipeline_lsp_surface_persisted_and_body_edit_invariant) { PASS(); } +TEST(pipeline_lsp_surface_decode_preserves_array_order_and_nulls) { + const char *json = + "{\"v\":1,\"lsp\":[" + "{\"qn\":\"first\",\"sn\":\"one\",\"lb\":\"Function\",\"rt\":null,\"spt\":[\"A\",null,\"C\"]}," + "{\"qn\":\"second\",\"sn\":\"two\",\"lb\":\"Method\",\"rt\":\"Receiver\",\"spt\":[\"D\"]}," + "{\"qn\":\"third\",\"sn\":\"three\",\"lb\":\"Class\",\"rt\":null,\"spt\":[]}]}"; + CBMArena arena; + cbm_arena_init(&arena); + CBMLSPDef *defs = NULL; + ASSERT_EQ(cbm_lsp_surface_defs_from_json(&arena, json, &defs), 3); + ASSERT_NOT_NULL(defs); + ASSERT_STR_EQ(defs[0].qualified_name, "first"); + ASSERT_STR_EQ(defs[1].qualified_name, "second"); + ASSERT_STR_EQ(defs[2].qualified_name, "third"); + ASSERT_TRUE(defs[0].receiver_type == NULL); + ASSERT_STR_EQ(defs[1].receiver_type, "Receiver"); + ASSERT_EQ(defs[0].signature_param_count, 3); + ASSERT_STR_EQ(defs[0].signature_param_types[0], "A"); + ASSERT_STR_EQ(defs[0].signature_param_types[1], "?"); + ASSERT_STR_EQ(defs[0].signature_param_types[2], "C"); + ASSERT_EQ(defs[2].signature_param_count, 0); + cbm_arena_destroy(&arena); + PASS(); +} + TEST(pipeline_ensemble_routing_edges) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_ens_XXXXXX"); @@ -13662,6 +13689,7 @@ TEST(pipeline_objectscript_export_range_join_keeps_one_trailing_marker) { #endif SUITE(pipeline) { + RUN_TEST(pipeline_lsp_surface_decode_preserves_array_order_and_nulls); RUN_TEST(pipeline_lsp_surface_persisted_and_body_edit_invariant); /* Index lock */ RUN_TEST(pipeline_lock_try_acquire);