diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index c9276df5f..1acd9d93b 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -9888,14 +9888,22 @@ static bool elixir_is_branch_operator(TSNode node, const char *source) { elixir_node_text_is(op, source, "when"); } +/* The pending frames live on the same growable stack as cbm_compute_complexity: + * a small on-stack first chunk that spills to the heap through + * cbm_walk_stack_reserve, so a wide body is never truncated and no thread pays + * for a large fixed array. If the stack cannot grow, every metric reports + * CBM_WALK_METRIC_UNAVAILABLE instead of a silently low count. */ static void elixir_compute_complexity(TSNode node, const char *source, cbm_complexity_t *out) { - enum { ELIXIR_CX_STACK_CAP = 4096 }; - struct { + enum { ELIXIR_CX_STACK_INLINE = 512 }; + struct elixir_cx_frame { TSNode node; int bdepth; int ldepth; int adepth; - } stack[ELIXIR_CX_STACK_CAP]; + }; + struct elixir_cx_frame inline_stack[ELIXIR_CX_STACK_INLINE]; + struct elixir_cx_frame *stack = inline_stack; + int cap = ELIXIR_CX_STACK_INLINE; memset(out, 0, sizeof(*out)); int top = 0; stack[top].node = node; @@ -9943,7 +9951,17 @@ static void elixir_compute_complexity(TSNode node, const char *source, cbm_compl child_l = d; } uint32_t nc = ts_node_child_count(n); - for (int i = (int)nc - 1; i >= 0 && top < ELIXIR_CX_STACK_CAP; i--) { + if (!cbm_walk_stack_reserve((void **)&stack, &cap, top + (int)nc, + sizeof(struct elixir_cx_frame), inline_stack, + "elixir_compute_complexity")) { + out->cyclomatic = CBM_WALK_METRIC_UNAVAILABLE; + out->cognitive = CBM_WALK_METRIC_UNAVAILABLE; + out->loop_count = CBM_WALK_METRIC_UNAVAILABLE; + out->loop_depth = CBM_WALK_METRIC_UNAVAILABLE; + out->max_access_depth = CBM_WALK_METRIC_UNAVAILABLE; + break; + } + for (int i = (int)nc - 1; i >= 0; i--) { stack[top].node = ts_node_child(n, (uint32_t)i); stack[top].bdepth = child_b; stack[top].ldepth = child_l; @@ -9951,6 +9969,7 @@ static void elixir_compute_complexity(TSNode node, const char *source, cbm_compl top++; } } + cbm_walk_stack_release(stack, inline_stack); } static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char *macro, diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 7b921b468..8e7231ccd 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -2810,6 +2810,40 @@ TEST(elixir_complexity_and_fingerprint) { PASS(); } +/* The Elixir complexity walk keeps its pending nodes on the growable stack + * cbm_compute_complexity uses. A fixed 4096-frame stack dropped the FIRST + * children of any wider body (they are pushed last-to-first), so the largest + * functions silently under-reported. This body is wider than that old cap. */ +enum { ELIXIR_WIDE_STMTS = 5000 }; + +TEST(elixir_complexity_wide_body_exceeds_old_stack_cap) { + static const char head[] = "defmodule Wide do\n def wide(x) do\n"; + static const char if_stmt[] = " if x, do: :a\n"; + static const char for_stmt[] = " for y <- x, do: y\n"; + static const char tail[] = " end\nend\n"; + size_t cap = sizeof(head) + (size_t)ELIXIR_WIDE_STMTS * sizeof(for_stmt) + sizeof(tail); + char *src = malloc(cap); + ASSERT_NOT_NULL(src); + size_t pos = (size_t)snprintf(src, cap, "%s", head); + for (int i = 0; i < ELIXIR_WIDE_STMTS; i++) { + pos += (size_t)snprintf(src + pos, cap - pos, "%s", (i % 2 == 0) ? if_stmt : for_stmt); + } + snprintf(src + pos, cap - pos, "%s", tail); + CBMFileResult *r = extract(src, CBM_LANG_ELIXIR, "t", "wide.ex"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + const CBMDefinition *d = elixir_first_def(r, "wide"); + ASSERT_NOT_NULL(d); + /* every `if` and every `for` is one branching call at nesting depth 0 */ + ASSERT_EQ(ELIXIR_WIDE_STMTS, d->complexity); + ASSERT_EQ(ELIXIR_WIDE_STMTS, d->cognitive); + ASSERT_EQ(ELIXIR_WIDE_STMTS / 2, d->loop_count); + ASSERT_EQ(1, d->loop_depth); + cbm_free_result(r); + free(src); + PASS(); +} + /* --- Haskell --- */ TEST(haskell_function) { CBMFileResult *r = extract("add :: Int -> Int -> Int\nadd x y = x + y\n\nmultiply :: Int -> " @@ -14615,6 +14649,7 @@ SUITE(extraction) { RUN_TEST(extract_elixir_guarded_def_head_guard_usages); RUN_TEST(elixir_channels_pubsub_and_handle_in); RUN_TEST(elixir_complexity_and_fingerprint); + RUN_TEST(elixir_complexity_wide_body_exceeds_old_stack_cap); RUN_TEST(haskell_function); RUN_TEST(haskell_infix_definition_name); RUN_TEST(ocaml_function);