mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-30 07:23:47 +08:00
Merge pull request #2079 from DeusData/fix/complexity-pass-deterministic-order
fix(pipeline): make the complexity pass independent of worker id order
This commit is contained in:
+156
-56
@@ -99,61 +99,166 @@ static void append_complexity_props(cbm_gbuf_node_t *node, int tld, bool recursi
|
||||
node->properties_json = neu;
|
||||
}
|
||||
|
||||
/* Content-only node order: qualified_name, then file path and start line.
|
||||
* Never the temp id: extract workers draw ids from one shared counter, so id
|
||||
* order is worker-scheduling order and differs run to run. The cycle guard
|
||||
* flags whichever member the DFS ENTERS first, so both the seed order and the
|
||||
* callee order must be a function of the inputs alone, or `recursive` flips
|
||||
* between otherwise identical multi-worker runs. A dangling target (no node
|
||||
* for the id) has no edges of its own, so where it sorts cannot move a flag;
|
||||
* it keys as empty strings. */
|
||||
enum { TLD_CMP_LESS = -1, TLD_CMP_GREATER = 1 };
|
||||
|
||||
static const char *str_or_empty(const char *s) {
|
||||
return s ? s : "";
|
||||
}
|
||||
|
||||
static int cmp_node_canonical(const cbm_gbuf_node_t *a, const cbm_gbuf_node_t *b) {
|
||||
int r = strcmp(str_or_empty(a ? a->qualified_name : NULL),
|
||||
str_or_empty(b ? b->qualified_name : NULL));
|
||||
if (r != 0) {
|
||||
return r;
|
||||
}
|
||||
r = strcmp(str_or_empty(a ? a->file_path : NULL), str_or_empty(b ? b->file_path : NULL));
|
||||
if (r != 0) {
|
||||
return r;
|
||||
}
|
||||
int la = a ? a->start_line : 0;
|
||||
int lb = b ? b->start_line : 0;
|
||||
if (la != lb) {
|
||||
return la < lb ? TLD_CMP_LESS : TLD_CMP_GREATER;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmp_seed_canonical(const void *pa, const void *pb) {
|
||||
return cmp_node_canonical(*(const cbm_gbuf_node_t *const *)pa,
|
||||
*(const cbm_gbuf_node_t *const *)pb);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const cbm_gbuf_node_t *node; /* NULL for a dangling target id */
|
||||
int64_t id;
|
||||
} tld_callee_t;
|
||||
|
||||
static int cmp_callee_canonical(const void *pa, const void *pb) {
|
||||
return cmp_node_canonical(((const tld_callee_t *)pa)->node, ((const tld_callee_t *)pb)->node);
|
||||
}
|
||||
|
||||
/* Traversal state. `callees` is one bump stack shared by every DFS frame: a
|
||||
* frame takes its out-degree worth of slots, sorts them, recurses, then
|
||||
* releases them. Nodes on the recursion path are distinct (state 1 blocks
|
||||
* re-entry), so the live slots never exceed the CALLS edge count the stack is
|
||||
* sized for. */
|
||||
typedef struct {
|
||||
const cbm_gbuf_t *gb;
|
||||
int64_t maxid;
|
||||
int *loop_depth;
|
||||
int *tld;
|
||||
char *state;
|
||||
bool *recursive;
|
||||
cbm_gbuf_node_t **seeds; /* Function/Method nodes, canonical order */
|
||||
int seed_count;
|
||||
int seed_cap;
|
||||
tld_callee_t *callees;
|
||||
int callee_top;
|
||||
int callee_cap;
|
||||
} tld_ctx_t;
|
||||
|
||||
static void tld_ctx_free(tld_ctx_t *cx) {
|
||||
free(cx->loop_depth);
|
||||
free(cx->tld);
|
||||
free(cx->state);
|
||||
free(cx->recursive);
|
||||
free(cx->seeds);
|
||||
free(cx->callees);
|
||||
}
|
||||
|
||||
/* Memoized DFS: tld(id) = loop_depth(id) + max over CALLS-callees of tld(callee).
|
||||
* state: 0=unvisited, 1=in-progress (back-edge → cycle), 2=done. */
|
||||
static int tld_dfs(const cbm_gbuf_t *gb, int64_t id, const int *loop_depth, int *tld, char *state,
|
||||
bool *recursive, int64_t maxid, int depth) {
|
||||
if (id < 1 || id > maxid) {
|
||||
* state: 0=unvisited, 1=in-progress (back-edge -> cycle), 2=done. */
|
||||
static int tld_dfs(tld_ctx_t *cx, int64_t id, int depth) {
|
||||
if (id < 1 || id > cx->maxid) {
|
||||
return 0;
|
||||
}
|
||||
if (state[id] == 2) {
|
||||
return tld[id];
|
||||
if (cx->state[id] == 2) {
|
||||
return cx->tld[id];
|
||||
}
|
||||
if (state[id] == 1) {
|
||||
recursive[id] = true; /* back edge → call-graph cycle */
|
||||
if (cx->state[id] == 1) {
|
||||
cx->recursive[id] = true; /* back edge -> call-graph cycle */
|
||||
return 0;
|
||||
}
|
||||
if (depth > CBM_TLD_MAX_DEPTH) {
|
||||
return loop_depth[id];
|
||||
return cx->loop_depth[id];
|
||||
}
|
||||
state[id] = 1;
|
||||
int best = 0;
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int ne = 0;
|
||||
cbm_gbuf_find_edges_by_source_type(gb, id, "CALLS", &edges, &ne);
|
||||
cbm_gbuf_find_edges_by_source_type(cx->gb, id, "CALLS", &edges, &ne);
|
||||
if (ne > cx->callee_cap - cx->callee_top) {
|
||||
return cx->loop_depth[id]; /* unreachable by construction; same as the depth cap */
|
||||
}
|
||||
cx->state[id] = 1;
|
||||
tld_callee_t *callees = cx->callees + cx->callee_top;
|
||||
int nc = 0;
|
||||
for (int i = 0; i < ne; i++) {
|
||||
int64_t c = edges[i]->target_id;
|
||||
if (c == id) {
|
||||
recursive[id] = true; /* direct self-recursion */
|
||||
cx->recursive[id] = true; /* direct self-recursion */
|
||||
continue;
|
||||
}
|
||||
int ct = tld_dfs(gb, c, loop_depth, tld, state, recursive, maxid, depth + 1);
|
||||
callees[nc].id = c;
|
||||
callees[nc].node = cbm_gbuf_find_by_id(cx->gb, c);
|
||||
nc++;
|
||||
}
|
||||
cx->callee_top += nc;
|
||||
qsort(callees, (size_t)nc, sizeof(*callees), cmp_callee_canonical);
|
||||
int best = 0;
|
||||
for (int i = 0; i < nc; i++) {
|
||||
int ct = tld_dfs(cx, callees[i].id, depth + 1);
|
||||
if (ct > best) {
|
||||
best = ct;
|
||||
}
|
||||
}
|
||||
tld[id] = loop_depth[id] + best;
|
||||
state[id] = 2;
|
||||
return tld[id];
|
||||
cx->callee_top -= nc;
|
||||
cx->tld[id] = cx->loop_depth[id] + best;
|
||||
cx->state[id] = 2;
|
||||
return cx->tld[id];
|
||||
}
|
||||
|
||||
/* Seed each Function/Method node's loop_depth and self_recursive flag, and
|
||||
* remember the node pointer for write-back. The self_recursive seed (set at
|
||||
* extraction) feeds the final recursive flag; tld_dfs additionally ORs in
|
||||
* mutual recursion discovered as a call-graph cycle. */
|
||||
static void seed_loop_depths(const cbm_gbuf_t *gb, const char *label, int *loop_depth,
|
||||
bool *recursive, cbm_gbuf_node_t **nptr, int64_t maxid) {
|
||||
static int label_count(const cbm_gbuf_t *gb, const char *label) {
|
||||
const cbm_gbuf_node_t **nodes = NULL;
|
||||
int count = 0;
|
||||
if (cbm_gbuf_find_by_label(gb, label, &nodes, &count) != 0) {
|
||||
return 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int calls_edge_count(const cbm_gbuf_t *gb) {
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int count = 0;
|
||||
if (cbm_gbuf_find_edges_by_type(gb, "CALLS", &edges, &count) != 0) {
|
||||
return 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Seed each Function/Method node's loop_depth and self_recursive flag, and
|
||||
* collect the node as a traversal seed (also the write-back target). The
|
||||
* self_recursive seed (set at extraction) feeds the final recursive flag;
|
||||
* tld_dfs additionally ORs in mutual recursion discovered as a call-graph
|
||||
* cycle. */
|
||||
static void seed_loop_depths(tld_ctx_t *cx, const char *label) {
|
||||
const cbm_gbuf_node_t **nodes = NULL;
|
||||
int count = 0;
|
||||
if (cbm_gbuf_find_by_label(cx->gb, label, &nodes, &count) != 0) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
for (int i = 0; i < count && cx->seed_count < cx->seed_cap; i++) {
|
||||
const cbm_gbuf_node_t *n = nodes[i];
|
||||
if (n->id >= 1 && n->id <= maxid) {
|
||||
loop_depth[n->id] = json_get_int(n->properties_json, "loop_depth", 0);
|
||||
recursive[n->id] = json_get_bool(n->properties_json, "self_recursive");
|
||||
nptr[n->id] = (cbm_gbuf_node_t *)n;
|
||||
if (n->id >= 1 && n->id <= cx->maxid) {
|
||||
cx->loop_depth[n->id] = json_get_int(n->properties_json, "loop_depth", 0);
|
||||
cx->recursive[n->id] = json_get_bool(n->properties_json, "self_recursive");
|
||||
cx->seeds[cx->seed_count++] = (cbm_gbuf_node_t *)n;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,40 +273,35 @@ void cbm_pipeline_pass_complexity(cbm_pipeline_ctx_t *ctx) {
|
||||
return;
|
||||
}
|
||||
size_t sz = (size_t)maxid + 1;
|
||||
int *loop_depth = calloc(sz, sizeof(int));
|
||||
int *tld = calloc(sz, sizeof(int));
|
||||
char *state = calloc(sz, sizeof(char));
|
||||
bool *recursive = calloc(sz, sizeof(bool));
|
||||
cbm_gbuf_node_t **nptr = calloc(sz, sizeof(cbm_gbuf_node_t *));
|
||||
if (!loop_depth || !tld || !state || !recursive || !nptr) {
|
||||
free(loop_depth);
|
||||
free(tld);
|
||||
free(state);
|
||||
free(recursive);
|
||||
free(nptr);
|
||||
tld_ctx_t cx = {0};
|
||||
cx.gb = gb;
|
||||
cx.maxid = maxid;
|
||||
cx.seed_cap = label_count(gb, "Function") + label_count(gb, "Method");
|
||||
cx.callee_cap = calls_edge_count(gb);
|
||||
cx.loop_depth = calloc(sz, sizeof(int));
|
||||
cx.tld = calloc(sz, sizeof(int));
|
||||
cx.state = calloc(sz, sizeof(char));
|
||||
cx.recursive = calloc(sz, sizeof(bool));
|
||||
cx.seeds = calloc((size_t)cx.seed_cap + 1, sizeof(cbm_gbuf_node_t *));
|
||||
cx.callees = calloc((size_t)cx.callee_cap + 1, sizeof(tld_callee_t));
|
||||
if (!cx.loop_depth || !cx.tld || !cx.state || !cx.recursive || !cx.seeds || !cx.callees) {
|
||||
tld_ctx_free(&cx);
|
||||
return;
|
||||
}
|
||||
|
||||
seed_loop_depths(gb, "Function", loop_depth, recursive, nptr, maxid);
|
||||
seed_loop_depths(gb, "Method", loop_depth, recursive, nptr, maxid);
|
||||
seed_loop_depths(&cx, "Function");
|
||||
seed_loop_depths(&cx, "Method");
|
||||
qsort(cx.seeds, (size_t)cx.seed_count, sizeof(*cx.seeds), cmp_seed_canonical);
|
||||
|
||||
int updated = 0;
|
||||
for (int64_t id = 1; id <= maxid; id++) {
|
||||
if (!nptr[id]) {
|
||||
continue; /* only Function/Method nodes */
|
||||
for (int i = 0; i < cx.seed_count; i++) {
|
||||
cbm_gbuf_node_t *n = cx.seeds[i];
|
||||
if (cx.state[n->id] != 2) {
|
||||
tld_dfs(&cx, n->id, 0);
|
||||
}
|
||||
if (state[id] != 2) {
|
||||
tld_dfs(gb, id, loop_depth, tld, state, recursive, maxid, 0);
|
||||
}
|
||||
append_complexity_props(nptr[id], tld[id], recursive[id]);
|
||||
updated++;
|
||||
append_complexity_props(n, cx.tld[n->id], cx.recursive[n->id]);
|
||||
}
|
||||
|
||||
cbm_log_info("pass.complexity", "functions", itoa_cx(updated));
|
||||
cbm_log_info("pass.complexity", "functions", itoa_cx(cx.seed_count));
|
||||
|
||||
free(loop_depth);
|
||||
free(tld);
|
||||
free(state);
|
||||
free(recursive);
|
||||
free(nptr);
|
||||
tld_ctx_free(&cx);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from p00_b import pair00_b
|
||||
|
||||
def pair00_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair00_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p00_a import pair00_a
|
||||
|
||||
def pair00_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair00_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p01_b import pair01_b
|
||||
|
||||
def pair01_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair01_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p01_a import pair01_a
|
||||
|
||||
def pair01_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair01_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p02_b import pair02_b
|
||||
|
||||
def pair02_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair02_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p02_a import pair02_a
|
||||
|
||||
def pair02_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair02_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p03_b import pair03_b
|
||||
|
||||
def pair03_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair03_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p03_a import pair03_a
|
||||
|
||||
def pair03_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair03_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p04_b import pair04_b
|
||||
|
||||
def pair04_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair04_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p04_a import pair04_a
|
||||
|
||||
def pair04_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair04_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p05_b import pair05_b
|
||||
|
||||
def pair05_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair05_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p05_a import pair05_a
|
||||
|
||||
def pair05_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair05_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p06_b import pair06_b
|
||||
|
||||
def pair06_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair06_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p06_a import pair06_a
|
||||
|
||||
def pair06_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair06_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p07_b import pair07_b
|
||||
|
||||
def pair07_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair07_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p07_a import pair07_a
|
||||
|
||||
def pair07_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair07_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p08_b import pair08_b
|
||||
|
||||
def pair08_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair08_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p08_a import pair08_a
|
||||
|
||||
def pair08_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair08_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p09_b import pair09_b
|
||||
|
||||
def pair09_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair09_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p09_a import pair09_a
|
||||
|
||||
def pair09_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair09_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p10_b import pair10_b
|
||||
|
||||
def pair10_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair10_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p10_a import pair10_a
|
||||
|
||||
def pair10_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair10_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p11_b import pair11_b
|
||||
|
||||
def pair11_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair11_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p11_a import pair11_a
|
||||
|
||||
def pair11_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair11_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p12_b import pair12_b
|
||||
|
||||
def pair12_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair12_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p12_a import pair12_a
|
||||
|
||||
def pair12_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair12_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p13_b import pair13_b
|
||||
|
||||
def pair13_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair13_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p13_a import pair13_a
|
||||
|
||||
def pair13_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair13_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p14_b import pair14_b
|
||||
|
||||
def pair14_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair14_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p14_a import pair14_a
|
||||
|
||||
def pair14_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair14_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p15_b import pair15_b
|
||||
|
||||
def pair15_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair15_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p15_a import pair15_a
|
||||
|
||||
def pair15_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair15_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p16_b import pair16_b
|
||||
|
||||
def pair16_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair16_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p16_a import pair16_a
|
||||
|
||||
def pair16_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair16_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p17_b import pair17_b
|
||||
|
||||
def pair17_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair17_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p17_a import pair17_a
|
||||
|
||||
def pair17_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair17_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p18_b import pair18_b
|
||||
|
||||
def pair18_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair18_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p18_a import pair18_a
|
||||
|
||||
def pair18_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair18_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p19_b import pair19_b
|
||||
|
||||
def pair19_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair19_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p19_a import pair19_a
|
||||
|
||||
def pair19_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair19_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p20_b import pair20_b
|
||||
|
||||
def pair20_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair20_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p20_a import pair20_a
|
||||
|
||||
def pair20_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair20_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p21_b import pair21_b
|
||||
|
||||
def pair21_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair21_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p21_a import pair21_a
|
||||
|
||||
def pair21_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair21_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p22_b import pair22_b
|
||||
|
||||
def pair22_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair22_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p22_a import pair22_a
|
||||
|
||||
def pair22_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair22_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p23_b import pair23_b
|
||||
|
||||
def pair23_a(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return pair23_b(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from p23_a import pair23_a
|
||||
|
||||
def pair23_b(n):
|
||||
if n <= 0:
|
||||
return 0
|
||||
return pair23_a(n)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t00_y import tri00_y
|
||||
|
||||
def tri00_x(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri00_y(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t00_z import tri00_z
|
||||
|
||||
def tri00_y(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri00_z(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t00_x import tri00_x
|
||||
|
||||
def tri00_z(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri00_x(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t01_y import tri01_y
|
||||
|
||||
def tri01_x(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri01_y(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t01_z import tri01_z
|
||||
|
||||
def tri01_y(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri01_z(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t01_x import tri01_x
|
||||
|
||||
def tri01_z(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri01_x(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t02_y import tri02_y
|
||||
|
||||
def tri02_x(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri02_y(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t02_z import tri02_z
|
||||
|
||||
def tri02_y(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri02_z(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t02_x import tri02_x
|
||||
|
||||
def tri02_z(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri02_x(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t03_y import tri03_y
|
||||
|
||||
def tri03_x(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri03_y(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t03_z import tri03_z
|
||||
|
||||
def tri03_y(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri03_z(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t03_x import tri03_x
|
||||
|
||||
def tri03_z(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri03_x(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t04_y import tri04_y
|
||||
|
||||
def tri04_x(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri04_y(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t04_z import tri04_z
|
||||
|
||||
def tri04_y(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri04_z(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t04_x import tri04_x
|
||||
|
||||
def tri04_z(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri04_x(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t05_y import tri05_y
|
||||
|
||||
def tri05_x(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri05_y(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t05_z import tri05_z
|
||||
|
||||
def tri05_y(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri05_z(n - 1)
|
||||
@@ -0,0 +1,6 @@
|
||||
from t05_x import tri05_x
|
||||
|
||||
def tri05_z(n):
|
||||
for _ in range(n):
|
||||
pass
|
||||
return tri05_x(n - 1)
|
||||
@@ -2066,6 +2066,226 @@ TEST(pipeline_call_reference_sequential_parallel_edge_set_parity) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Reproduce-first for the multi-worker determinism gap distilled from #1925:
|
||||
* the complexity pass walked Function/Method nodes and their CALLS targets in
|
||||
* temp-id order. Extract workers draw ids from one shared counter, so that
|
||||
* order is worker-scheduling order, and the cycle guard flags whichever member
|
||||
* of a mutual-recursion cycle the DFS enters first. Run to run, `recursive`
|
||||
* therefore flipped between the members of a cycle while the CALLS edge set
|
||||
* stayed identical -- a violation of the MT-byte-identical invariant. The
|
||||
* fixture holds 24 two-member and 6 three-member cycles, one function per
|
||||
* file, whose members are equal-sized (adjacent in the size-ordered work
|
||||
* queue), which is what makes the flip likely. The
|
||||
* fixture exceeds MIN_FILES_FOR_PARALLEL; CBM_INDEX_SINGLE_THREAD forces the
|
||||
* reference run through the sequential path and CBM_WORKERS forces the
|
||||
* repeated runs through pass_parallel.c, exactly like the parity test above.
|
||||
* Every multi-worker run must match the sequential one byte for byte. */
|
||||
enum { CX_ORDER_MT_RUNS = 6, CX_ORDER_LINE_MAX = 512 };
|
||||
|
||||
static const char *cx_order_fixture_dir(void) {
|
||||
return "tests/fixtures/complexity_pass_cycle_order";
|
||||
}
|
||||
|
||||
/* Copy the regular files of a flat fixture directory into dst_dir. Returns the
|
||||
* number of files copied, or -1 when one could not be read whole. */
|
||||
static int cx_order_copy_fixture(const char *src_dir, const char *dst_dir) {
|
||||
cbm_dir_t *d = cbm_opendir(src_dir);
|
||||
if (!d) {
|
||||
return -1;
|
||||
}
|
||||
int copied = 0;
|
||||
cbm_dirent_t *entry;
|
||||
while ((entry = cbm_readdir(d)) != NULL) {
|
||||
if (entry->name[0] == '.' || entry->is_dir) {
|
||||
continue;
|
||||
}
|
||||
char src[CBM_SZ_1K];
|
||||
snprintf(src, sizeof(src), "%s/%s", src_dir, entry->name);
|
||||
FILE *in = cbm_fopen(src, "rb");
|
||||
if (!in) {
|
||||
copied = -1;
|
||||
break;
|
||||
}
|
||||
char buf[CBM_SZ_4K];
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, in);
|
||||
fclose(in);
|
||||
if (n == sizeof(buf) - 1) {
|
||||
copied = -1; /* fixture files are tiny; a full buffer means truncation */
|
||||
break;
|
||||
}
|
||||
buf[n] = '\0';
|
||||
write_temp_file(dst_dir, entry->name, buf);
|
||||
copied++;
|
||||
}
|
||||
cbm_closedir(d);
|
||||
return copied;
|
||||
}
|
||||
|
||||
static int cx_order_cmp_node_qn(const void *pa, const void *pb) {
|
||||
const cbm_node_t *a = *(const cbm_node_t *const *)pa;
|
||||
const cbm_node_t *b = *(const cbm_node_t *const *)pb;
|
||||
return strcmp(a->qualified_name ? a->qualified_name : "",
|
||||
b->qualified_name ? b->qualified_name : "");
|
||||
}
|
||||
|
||||
/* One line per Function in qualified-name order: "<qn> <tld> <recursive>".
|
||||
* Sorting by name keeps DB ids and row order out of the comparison. Returns a
|
||||
* heap string, or NULL when the store cannot be read. */
|
||||
static char *cx_order_signature(const char *db_path, const char *project, int *func_count) {
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
cbm_node_t *funcs = NULL;
|
||||
int count = 0;
|
||||
if (cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &count) != CBM_STORE_OK) {
|
||||
cbm_store_close(s);
|
||||
return NULL;
|
||||
}
|
||||
const cbm_node_t **sorted = calloc((size_t)count + 1, sizeof(*sorted));
|
||||
char *sig = calloc((size_t)count + 1, CX_ORDER_LINE_MAX);
|
||||
if (sorted && sig) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
sorted[i] = &funcs[i];
|
||||
}
|
||||
qsort(sorted, (size_t)count, sizeof(*sorted), cx_order_cmp_node_qn);
|
||||
size_t used = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *props = sorted[i]->properties_json ? sorted[i]->properties_json : "{}";
|
||||
const char *tld = strstr(props, "\"transitive_loop_depth\":");
|
||||
const char *rec = strstr(props, "\"recursive\":");
|
||||
int w = snprintf(sig + used, CX_ORDER_LINE_MAX, "%s %.*s %.*s\n",
|
||||
sorted[i]->qualified_name ? sorted[i]->qualified_name : "",
|
||||
tld ? (int)strcspn(tld, ",}") : 0, tld ? tld : "",
|
||||
rec ? (int)strcspn(rec, ",}") : 0, rec ? rec : "");
|
||||
if (w < 0) {
|
||||
w = 0;
|
||||
} else if (w >= CX_ORDER_LINE_MAX) {
|
||||
w = CX_ORDER_LINE_MAX - 1;
|
||||
}
|
||||
used += (size_t)w;
|
||||
}
|
||||
} else {
|
||||
free(sig);
|
||||
sig = NULL;
|
||||
}
|
||||
free(sorted);
|
||||
cbm_store_free_nodes(funcs, count);
|
||||
cbm_store_close(s);
|
||||
*func_count = count;
|
||||
return sig;
|
||||
}
|
||||
|
||||
/* First line of `got` that differs from `want`, copied into out (for the
|
||||
* failure diagnostic). Returns false when the strings are identical. */
|
||||
static bool cx_order_first_diff(const char *want, const char *got, char *out, size_t cap) {
|
||||
if (strcmp(want, got) == 0) {
|
||||
return false;
|
||||
}
|
||||
while (*want && *got) {
|
||||
size_t lw = strcspn(want, "\n");
|
||||
size_t lg = strcspn(got, "\n");
|
||||
if (lw != lg || memcmp(want, got, lw) != 0) {
|
||||
snprintf(out, cap, "want '%.*s' got '%.*s'", (int)lw, want, (int)lg, got);
|
||||
return true;
|
||||
}
|
||||
want += lw + (want[lw] == '\n');
|
||||
got += lg + (got[lg] == '\n');
|
||||
}
|
||||
snprintf(out, cap, "line count differs");
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(pipeline_complexity_props_independent_of_worker_order) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_cx_order_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
int copied = cx_order_copy_fixture(cx_order_fixture_dir(), tmp);
|
||||
if (copied <= 0) {
|
||||
th_rmtree(tmp);
|
||||
FAIL("fixture copy");
|
||||
}
|
||||
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/cx_sequential.db", tmp);
|
||||
cbm_pipeline_t *sequential = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
int sequential_rc = sequential ? cbm_pipeline_run(sequential) : -1;
|
||||
int sequential_funcs = 0;
|
||||
char *sequential_sig = NULL;
|
||||
if (sequential && sequential_rc == 0) {
|
||||
sequential_sig =
|
||||
cx_order_signature(db_path, cbm_pipeline_project_name(sequential), &sequential_funcs);
|
||||
}
|
||||
cbm_pipeline_free(sequential);
|
||||
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
int parallel_rc[CX_ORDER_MT_RUNS];
|
||||
char *parallel_sig[CX_ORDER_MT_RUNS];
|
||||
for (int r = 0; r < CX_ORDER_MT_RUNS; r++) {
|
||||
snprintf(db_path, sizeof(db_path), "%s/cx_parallel_%d.db", tmp, r);
|
||||
cbm_pipeline_t *parallel = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
parallel_rc[r] = parallel ? cbm_pipeline_run(parallel) : -1;
|
||||
parallel_sig[r] = NULL;
|
||||
if (parallel && parallel_rc[r] == 0) {
|
||||
int funcs = 0;
|
||||
parallel_sig[r] =
|
||||
cx_order_signature(db_path, cbm_pipeline_project_name(parallel), &funcs);
|
||||
}
|
||||
cbm_pipeline_free(parallel);
|
||||
}
|
||||
|
||||
if (saved_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
free(saved_workers);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
if (saved_single) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
|
||||
free(saved_single);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
|
||||
/* Verdicts first, then release, then assert: the assertions must not leak. */
|
||||
bool cycles_detected = sequential_sig && strstr(sequential_sig, "\"recursive\":true") != NULL;
|
||||
int mismatch_run = -1;
|
||||
char diff[CBM_SZ_1K] = "";
|
||||
for (int r = 0; r < CX_ORDER_MT_RUNS && mismatch_run < 0; r++) {
|
||||
if (parallel_rc[r] != 0 || !parallel_sig[r]) {
|
||||
mismatch_run = r;
|
||||
snprintf(diff, sizeof(diff), "run %d rc=%d", r, parallel_rc[r]);
|
||||
} else if (sequential_sig &&
|
||||
cx_order_first_diff(sequential_sig, parallel_sig[r], diff, sizeof(diff))) {
|
||||
mismatch_run = r;
|
||||
}
|
||||
}
|
||||
free(sequential_sig);
|
||||
for (int r = 0; r < CX_ORDER_MT_RUNS; r++) {
|
||||
free(parallel_sig[r]);
|
||||
}
|
||||
|
||||
ASSERT_EQ(sequential_rc, 0);
|
||||
ASSERT_NOT_NULL(sequential_sig);
|
||||
ASSERT_GTE(sequential_funcs, copied); /* at least the one function per fixture file */
|
||||
ASSERT_TRUE(cycles_detected); /* the cycles must reach the pass at all */
|
||||
if (mismatch_run >= 0) {
|
||||
printf("\n parallel run %d diverges from sequential: %s\n", mismatch_run, diff);
|
||||
FAIL("complexity props depend on worker id order");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* utimensat/AT_FDCWD do not exist on Windows. Set the same instant through
|
||||
* SetFileTime: FILETIME is 100ns ticks since 1601, the same representation
|
||||
@@ -13587,6 +13807,7 @@ SUITE(pipeline) {
|
||||
#endif
|
||||
RUN_TEST(pipeline_env_access_configures_sequential_parallel_parity);
|
||||
RUN_TEST(pipeline_call_reference_sequential_parallel_edge_set_parity);
|
||||
RUN_TEST(pipeline_complexity_props_independent_of_worker_order);
|
||||
RUN_TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full);
|
||||
RUN_TEST(pipeline_incremental_changed_target_invalidates_stale_inbound_call_reference);
|
||||
RUN_TEST(pipeline_incremental_parallel_registry_nodes_advance_shared_ids);
|
||||
|
||||
Reference in New Issue
Block a user