mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(elixir): grow the complexity walk stack instead of capping it
elixir_compute_complexity kept its pending frames in one fixed 4096-entry array (48 bytes each, 196,608 bytes in a single stack frame) and stopped pushing children once it was full. Children are pushed last to first, so a wider body lost its FIRST children and the function reported a lower complexity with no signal, on exactly the largest functions. Worker threads do not get the 8 MB main-thread stack, so the array was a risk of its own. It now uses the walker pattern cbm_compute_complexity has used since #2375: a 512-frame on-stack first chunk that spills to the heap through cbm_walk_stack_reserve (memory core, class EXTRACT) and is released with cbm_walk_stack_release. If the stack cannot grow, every metric reports CBM_WALK_METRIC_UNAVAILABLE instead of an undercount. This answers items 2 and 3 of the 2026-08-20 review. The new test builds a 5000-statement body of alternating if and for. The fixed stack reports complexity 4095 for it instead of 5000. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
b19ac1e923
commit
aa3161fd81
2 files changed
+58
-4
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user