Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 156 additions & 56 deletions src/pipeline/pass_complexity.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand All @@ -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 */
}
if (state[id] != 2) {
tld_dfs(gb, id, loop_depth, tld, state, recursive, maxid, 0);
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);
}
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);
}
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p00_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p00_b import pair00_b

def pair00_a(n):
for _ in range(n):
pass
return pair00_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p00_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p00_a import pair00_a

def pair00_b(n):
if n <= 0:
return 0
return pair00_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p01_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p01_b import pair01_b

def pair01_a(n):
for _ in range(n):
pass
return pair01_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p01_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p01_a import pair01_a

def pair01_b(n):
if n <= 0:
return 0
return pair01_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p02_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p02_b import pair02_b

def pair02_a(n):
for _ in range(n):
pass
return pair02_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p02_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p02_a import pair02_a

def pair02_b(n):
if n <= 0:
return 0
return pair02_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p03_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p03_b import pair03_b

def pair03_a(n):
for _ in range(n):
pass
return pair03_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p03_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p03_a import pair03_a

def pair03_b(n):
if n <= 0:
return 0
return pair03_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p04_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p04_b import pair04_b

def pair04_a(n):
for _ in range(n):
pass
return pair04_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p04_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p04_a import pair04_a

def pair04_b(n):
if n <= 0:
return 0
return pair04_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p05_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p05_b import pair05_b

def pair05_a(n):
for _ in range(n):
pass
return pair05_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p05_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p05_a import pair05_a

def pair05_b(n):
if n <= 0:
return 0
return pair05_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p06_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p06_b import pair06_b

def pair06_a(n):
for _ in range(n):
pass
return pair06_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p06_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p06_a import pair06_a

def pair06_b(n):
if n <= 0:
return 0
return pair06_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p07_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p07_b import pair07_b

def pair07_a(n):
for _ in range(n):
pass
return pair07_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p07_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p07_a import pair07_a

def pair07_b(n):
if n <= 0:
return 0
return pair07_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p08_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p08_b import pair08_b

def pair08_a(n):
for _ in range(n):
pass
return pair08_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p08_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p08_a import pair08_a

def pair08_b(n):
if n <= 0:
return 0
return pair08_a(n)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p09_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p09_b import pair09_b

def pair09_a(n):
for _ in range(n):
pass
return pair09_b(n - 1)
6 changes: 6 additions & 0 deletions tests/fixtures/complexity_pass_cycle_order/p09_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from p09_a import pair09_a

def pair09_b(n):
if n <= 0:
return 0
return pair09_a(n)
Loading
Loading