From e410c86f270a013d79639577d82009698b207e8b Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 7 Sep 2026 00:32:02 +0200 Subject: [PATCH] fix(pipeline): make the complexity pass independent of worker id order The complexity pass (transitive_loop_depth / recursive) walked Function and Method nodes in temp-id order and each node's CALLS targets in adjacency order. Under parallel extraction the temp ids come from one shared atomic counter, so id order is worker-scheduling order and differs run to run. The cycle guard flags whichever member of a mutual-recursion cycle the DFS enters first, so `recursive` flipped between the members of a cycle across otherwise identical multi-worker runs while the CALLS edge set stayed identical -- a violation of the MT-byte-identical invariant. Evidence: on a 66-file Python fixture of equal-sized mutual-recursion cycles (24 pairs + 6 triangles, adjacent in the size-ordered work queue) a probe saw 3 distinct outputs in 5 runs at 4 workers. The new regression test is RED on main 5/5 (a 4-worker run diverges from the single-threaded run on the first cycle every time) and GREEN with this change. Fix: order the traversal by content only -- seeds sorted by (qualified_name, file_path, start_line), and each node's CALLS targets sorted the same way before recursing, through one bump-stack scratch sized to the CALLS edge count (a single upfront allocation that bails the way the existing calloc path does). Semantics are unchanged: the DFS entry node of a cycle is the one flagged, as before; which member that is now depends on the inputs alone, identically for sequential and parallel indexing. SCC-based marking of every cycle member is out of scope here. Test: pipeline_complexity_props_independent_of_worker_order indexes the fixture once single-threaded and six times with CBM_WORKERS=4 through the same harness as the sequential/parallel parity test, collects every Function's complexity props sorted by qualified_name, and asserts all parallel runs are identical to the sequential run. Distilled from #1925 with co-author credit. Verification: build/c/test-runner pipeline complexity extraction (ASan/UBSan) 617 passed, 0 failed; revert-check RED 5/5 on main, GREEN with the fix; make -f Makefile.cbm lint-ci clean; scripts/check-no-test-skips.sh OK. Signed-off-by: Martin Vogel Co-authored-by: Zhiyu --- src/pipeline/pass_complexity.c | 212 ++++++++++++----- .../complexity_pass_cycle_order/p00_a.py | 6 + .../complexity_pass_cycle_order/p00_b.py | 6 + .../complexity_pass_cycle_order/p01_a.py | 6 + .../complexity_pass_cycle_order/p01_b.py | 6 + .../complexity_pass_cycle_order/p02_a.py | 6 + .../complexity_pass_cycle_order/p02_b.py | 6 + .../complexity_pass_cycle_order/p03_a.py | 6 + .../complexity_pass_cycle_order/p03_b.py | 6 + .../complexity_pass_cycle_order/p04_a.py | 6 + .../complexity_pass_cycle_order/p04_b.py | 6 + .../complexity_pass_cycle_order/p05_a.py | 6 + .../complexity_pass_cycle_order/p05_b.py | 6 + .../complexity_pass_cycle_order/p06_a.py | 6 + .../complexity_pass_cycle_order/p06_b.py | 6 + .../complexity_pass_cycle_order/p07_a.py | 6 + .../complexity_pass_cycle_order/p07_b.py | 6 + .../complexity_pass_cycle_order/p08_a.py | 6 + .../complexity_pass_cycle_order/p08_b.py | 6 + .../complexity_pass_cycle_order/p09_a.py | 6 + .../complexity_pass_cycle_order/p09_b.py | 6 + .../complexity_pass_cycle_order/p10_a.py | 6 + .../complexity_pass_cycle_order/p10_b.py | 6 + .../complexity_pass_cycle_order/p11_a.py | 6 + .../complexity_pass_cycle_order/p11_b.py | 6 + .../complexity_pass_cycle_order/p12_a.py | 6 + .../complexity_pass_cycle_order/p12_b.py | 6 + .../complexity_pass_cycle_order/p13_a.py | 6 + .../complexity_pass_cycle_order/p13_b.py | 6 + .../complexity_pass_cycle_order/p14_a.py | 6 + .../complexity_pass_cycle_order/p14_b.py | 6 + .../complexity_pass_cycle_order/p15_a.py | 6 + .../complexity_pass_cycle_order/p15_b.py | 6 + .../complexity_pass_cycle_order/p16_a.py | 6 + .../complexity_pass_cycle_order/p16_b.py | 6 + .../complexity_pass_cycle_order/p17_a.py | 6 + .../complexity_pass_cycle_order/p17_b.py | 6 + .../complexity_pass_cycle_order/p18_a.py | 6 + .../complexity_pass_cycle_order/p18_b.py | 6 + .../complexity_pass_cycle_order/p19_a.py | 6 + .../complexity_pass_cycle_order/p19_b.py | 6 + .../complexity_pass_cycle_order/p20_a.py | 6 + .../complexity_pass_cycle_order/p20_b.py | 6 + .../complexity_pass_cycle_order/p21_a.py | 6 + .../complexity_pass_cycle_order/p21_b.py | 6 + .../complexity_pass_cycle_order/p22_a.py | 6 + .../complexity_pass_cycle_order/p22_b.py | 6 + .../complexity_pass_cycle_order/p23_a.py | 6 + .../complexity_pass_cycle_order/p23_b.py | 6 + .../complexity_pass_cycle_order/t00_x.py | 6 + .../complexity_pass_cycle_order/t00_y.py | 6 + .../complexity_pass_cycle_order/t00_z.py | 6 + .../complexity_pass_cycle_order/t01_x.py | 6 + .../complexity_pass_cycle_order/t01_y.py | 6 + .../complexity_pass_cycle_order/t01_z.py | 6 + .../complexity_pass_cycle_order/t02_x.py | 6 + .../complexity_pass_cycle_order/t02_y.py | 6 + .../complexity_pass_cycle_order/t02_z.py | 6 + .../complexity_pass_cycle_order/t03_x.py | 6 + .../complexity_pass_cycle_order/t03_y.py | 6 + .../complexity_pass_cycle_order/t03_z.py | 6 + .../complexity_pass_cycle_order/t04_x.py | 6 + .../complexity_pass_cycle_order/t04_y.py | 6 + .../complexity_pass_cycle_order/t04_z.py | 6 + .../complexity_pass_cycle_order/t05_x.py | 6 + .../complexity_pass_cycle_order/t05_y.py | 6 + .../complexity_pass_cycle_order/t05_z.py | 6 + tests/test_pipeline.c | 221 ++++++++++++++++++ 68 files changed, 773 insertions(+), 56 deletions(-) create mode 100644 tests/fixtures/complexity_pass_cycle_order/p00_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p00_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p01_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p01_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p02_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p02_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p03_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p03_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p04_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p04_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p05_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p05_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p06_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p06_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p07_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p07_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p08_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p08_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p09_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p09_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p10_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p10_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p11_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p11_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p12_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p12_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p13_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p13_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p14_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p14_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p15_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p15_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p16_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p16_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p17_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p17_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p18_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p18_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p19_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p19_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p20_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p20_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p21_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p21_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p22_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p22_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p23_a.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/p23_b.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t00_x.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t00_y.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t00_z.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t01_x.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t01_y.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t01_z.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t02_x.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t02_y.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t02_z.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t03_x.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t03_y.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t03_z.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t04_x.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t04_y.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t04_z.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t05_x.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t05_y.py create mode 100644 tests/fixtures/complexity_pass_cycle_order/t05_z.py diff --git a/src/pipeline/pass_complexity.c b/src/pipeline/pass_complexity.c index 4a3ebf85e..420237b81 100644 --- a/src/pipeline/pass_complexity.c +++ b/src/pipeline/pass_complexity.c @@ -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 */ - } - 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); } diff --git a/tests/fixtures/complexity_pass_cycle_order/p00_a.py b/tests/fixtures/complexity_pass_cycle_order/p00_a.py new file mode 100644 index 000000000..ff8d38616 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p00_a.py @@ -0,0 +1,6 @@ +from p00_b import pair00_b + +def pair00_a(n): + for _ in range(n): + pass + return pair00_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p00_b.py b/tests/fixtures/complexity_pass_cycle_order/p00_b.py new file mode 100644 index 000000000..5d2939ce9 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p00_b.py @@ -0,0 +1,6 @@ +from p00_a import pair00_a + +def pair00_b(n): + if n <= 0: + return 0 + return pair00_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p01_a.py b/tests/fixtures/complexity_pass_cycle_order/p01_a.py new file mode 100644 index 000000000..b602a8192 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p01_a.py @@ -0,0 +1,6 @@ +from p01_b import pair01_b + +def pair01_a(n): + for _ in range(n): + pass + return pair01_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p01_b.py b/tests/fixtures/complexity_pass_cycle_order/p01_b.py new file mode 100644 index 000000000..8ef871ebd --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p01_b.py @@ -0,0 +1,6 @@ +from p01_a import pair01_a + +def pair01_b(n): + if n <= 0: + return 0 + return pair01_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p02_a.py b/tests/fixtures/complexity_pass_cycle_order/p02_a.py new file mode 100644 index 000000000..f77ffe62a --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p02_a.py @@ -0,0 +1,6 @@ +from p02_b import pair02_b + +def pair02_a(n): + for _ in range(n): + pass + return pair02_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p02_b.py b/tests/fixtures/complexity_pass_cycle_order/p02_b.py new file mode 100644 index 000000000..a7e5baf30 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p02_b.py @@ -0,0 +1,6 @@ +from p02_a import pair02_a + +def pair02_b(n): + if n <= 0: + return 0 + return pair02_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p03_a.py b/tests/fixtures/complexity_pass_cycle_order/p03_a.py new file mode 100644 index 000000000..c59dad576 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p03_a.py @@ -0,0 +1,6 @@ +from p03_b import pair03_b + +def pair03_a(n): + for _ in range(n): + pass + return pair03_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p03_b.py b/tests/fixtures/complexity_pass_cycle_order/p03_b.py new file mode 100644 index 000000000..90f807bcd --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p03_b.py @@ -0,0 +1,6 @@ +from p03_a import pair03_a + +def pair03_b(n): + if n <= 0: + return 0 + return pair03_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p04_a.py b/tests/fixtures/complexity_pass_cycle_order/p04_a.py new file mode 100644 index 000000000..89a1385b0 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p04_a.py @@ -0,0 +1,6 @@ +from p04_b import pair04_b + +def pair04_a(n): + for _ in range(n): + pass + return pair04_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p04_b.py b/tests/fixtures/complexity_pass_cycle_order/p04_b.py new file mode 100644 index 000000000..a333f1079 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p04_b.py @@ -0,0 +1,6 @@ +from p04_a import pair04_a + +def pair04_b(n): + if n <= 0: + return 0 + return pair04_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p05_a.py b/tests/fixtures/complexity_pass_cycle_order/p05_a.py new file mode 100644 index 000000000..356bc4259 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p05_a.py @@ -0,0 +1,6 @@ +from p05_b import pair05_b + +def pair05_a(n): + for _ in range(n): + pass + return pair05_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p05_b.py b/tests/fixtures/complexity_pass_cycle_order/p05_b.py new file mode 100644 index 000000000..b51770ba7 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p05_b.py @@ -0,0 +1,6 @@ +from p05_a import pair05_a + +def pair05_b(n): + if n <= 0: + return 0 + return pair05_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p06_a.py b/tests/fixtures/complexity_pass_cycle_order/p06_a.py new file mode 100644 index 000000000..c01b33466 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p06_a.py @@ -0,0 +1,6 @@ +from p06_b import pair06_b + +def pair06_a(n): + for _ in range(n): + pass + return pair06_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p06_b.py b/tests/fixtures/complexity_pass_cycle_order/p06_b.py new file mode 100644 index 000000000..d13628e36 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p06_b.py @@ -0,0 +1,6 @@ +from p06_a import pair06_a + +def pair06_b(n): + if n <= 0: + return 0 + return pair06_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p07_a.py b/tests/fixtures/complexity_pass_cycle_order/p07_a.py new file mode 100644 index 000000000..9bf317024 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p07_a.py @@ -0,0 +1,6 @@ +from p07_b import pair07_b + +def pair07_a(n): + for _ in range(n): + pass + return pair07_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p07_b.py b/tests/fixtures/complexity_pass_cycle_order/p07_b.py new file mode 100644 index 000000000..b3478c704 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p07_b.py @@ -0,0 +1,6 @@ +from p07_a import pair07_a + +def pair07_b(n): + if n <= 0: + return 0 + return pair07_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p08_a.py b/tests/fixtures/complexity_pass_cycle_order/p08_a.py new file mode 100644 index 000000000..146b0886e --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p08_a.py @@ -0,0 +1,6 @@ +from p08_b import pair08_b + +def pair08_a(n): + for _ in range(n): + pass + return pair08_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p08_b.py b/tests/fixtures/complexity_pass_cycle_order/p08_b.py new file mode 100644 index 000000000..c97c509a0 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p08_b.py @@ -0,0 +1,6 @@ +from p08_a import pair08_a + +def pair08_b(n): + if n <= 0: + return 0 + return pair08_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p09_a.py b/tests/fixtures/complexity_pass_cycle_order/p09_a.py new file mode 100644 index 000000000..e03e52d92 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p09_a.py @@ -0,0 +1,6 @@ +from p09_b import pair09_b + +def pair09_a(n): + for _ in range(n): + pass + return pair09_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p09_b.py b/tests/fixtures/complexity_pass_cycle_order/p09_b.py new file mode 100644 index 000000000..4cf1149bd --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p09_b.py @@ -0,0 +1,6 @@ +from p09_a import pair09_a + +def pair09_b(n): + if n <= 0: + return 0 + return pair09_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p10_a.py b/tests/fixtures/complexity_pass_cycle_order/p10_a.py new file mode 100644 index 000000000..7c8e3efd5 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p10_a.py @@ -0,0 +1,6 @@ +from p10_b import pair10_b + +def pair10_a(n): + for _ in range(n): + pass + return pair10_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p10_b.py b/tests/fixtures/complexity_pass_cycle_order/p10_b.py new file mode 100644 index 000000000..ec23c5c37 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p10_b.py @@ -0,0 +1,6 @@ +from p10_a import pair10_a + +def pair10_b(n): + if n <= 0: + return 0 + return pair10_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p11_a.py b/tests/fixtures/complexity_pass_cycle_order/p11_a.py new file mode 100644 index 000000000..b12fab327 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p11_a.py @@ -0,0 +1,6 @@ +from p11_b import pair11_b + +def pair11_a(n): + for _ in range(n): + pass + return pair11_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p11_b.py b/tests/fixtures/complexity_pass_cycle_order/p11_b.py new file mode 100644 index 000000000..1cb4f26e5 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p11_b.py @@ -0,0 +1,6 @@ +from p11_a import pair11_a + +def pair11_b(n): + if n <= 0: + return 0 + return pair11_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p12_a.py b/tests/fixtures/complexity_pass_cycle_order/p12_a.py new file mode 100644 index 000000000..3cfdade22 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p12_a.py @@ -0,0 +1,6 @@ +from p12_b import pair12_b + +def pair12_a(n): + for _ in range(n): + pass + return pair12_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p12_b.py b/tests/fixtures/complexity_pass_cycle_order/p12_b.py new file mode 100644 index 000000000..43e53aa04 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p12_b.py @@ -0,0 +1,6 @@ +from p12_a import pair12_a + +def pair12_b(n): + if n <= 0: + return 0 + return pair12_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p13_a.py b/tests/fixtures/complexity_pass_cycle_order/p13_a.py new file mode 100644 index 000000000..7acb11ad7 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p13_a.py @@ -0,0 +1,6 @@ +from p13_b import pair13_b + +def pair13_a(n): + for _ in range(n): + pass + return pair13_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p13_b.py b/tests/fixtures/complexity_pass_cycle_order/p13_b.py new file mode 100644 index 000000000..f222cb03b --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p13_b.py @@ -0,0 +1,6 @@ +from p13_a import pair13_a + +def pair13_b(n): + if n <= 0: + return 0 + return pair13_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p14_a.py b/tests/fixtures/complexity_pass_cycle_order/p14_a.py new file mode 100644 index 000000000..46c15ab00 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p14_a.py @@ -0,0 +1,6 @@ +from p14_b import pair14_b + +def pair14_a(n): + for _ in range(n): + pass + return pair14_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p14_b.py b/tests/fixtures/complexity_pass_cycle_order/p14_b.py new file mode 100644 index 000000000..18e458f54 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p14_b.py @@ -0,0 +1,6 @@ +from p14_a import pair14_a + +def pair14_b(n): + if n <= 0: + return 0 + return pair14_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p15_a.py b/tests/fixtures/complexity_pass_cycle_order/p15_a.py new file mode 100644 index 000000000..542e43e58 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p15_a.py @@ -0,0 +1,6 @@ +from p15_b import pair15_b + +def pair15_a(n): + for _ in range(n): + pass + return pair15_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p15_b.py b/tests/fixtures/complexity_pass_cycle_order/p15_b.py new file mode 100644 index 000000000..e79bbf276 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p15_b.py @@ -0,0 +1,6 @@ +from p15_a import pair15_a + +def pair15_b(n): + if n <= 0: + return 0 + return pair15_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p16_a.py b/tests/fixtures/complexity_pass_cycle_order/p16_a.py new file mode 100644 index 000000000..f981a15de --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p16_a.py @@ -0,0 +1,6 @@ +from p16_b import pair16_b + +def pair16_a(n): + for _ in range(n): + pass + return pair16_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p16_b.py b/tests/fixtures/complexity_pass_cycle_order/p16_b.py new file mode 100644 index 000000000..2585aba96 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p16_b.py @@ -0,0 +1,6 @@ +from p16_a import pair16_a + +def pair16_b(n): + if n <= 0: + return 0 + return pair16_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p17_a.py b/tests/fixtures/complexity_pass_cycle_order/p17_a.py new file mode 100644 index 000000000..57c3cd1e5 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p17_a.py @@ -0,0 +1,6 @@ +from p17_b import pair17_b + +def pair17_a(n): + for _ in range(n): + pass + return pair17_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p17_b.py b/tests/fixtures/complexity_pass_cycle_order/p17_b.py new file mode 100644 index 000000000..94392a7ff --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p17_b.py @@ -0,0 +1,6 @@ +from p17_a import pair17_a + +def pair17_b(n): + if n <= 0: + return 0 + return pair17_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p18_a.py b/tests/fixtures/complexity_pass_cycle_order/p18_a.py new file mode 100644 index 000000000..55ef68989 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p18_a.py @@ -0,0 +1,6 @@ +from p18_b import pair18_b + +def pair18_a(n): + for _ in range(n): + pass + return pair18_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p18_b.py b/tests/fixtures/complexity_pass_cycle_order/p18_b.py new file mode 100644 index 000000000..ae2f67de9 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p18_b.py @@ -0,0 +1,6 @@ +from p18_a import pair18_a + +def pair18_b(n): + if n <= 0: + return 0 + return pair18_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p19_a.py b/tests/fixtures/complexity_pass_cycle_order/p19_a.py new file mode 100644 index 000000000..3641aa150 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p19_a.py @@ -0,0 +1,6 @@ +from p19_b import pair19_b + +def pair19_a(n): + for _ in range(n): + pass + return pair19_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p19_b.py b/tests/fixtures/complexity_pass_cycle_order/p19_b.py new file mode 100644 index 000000000..27b3cb8d0 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p19_b.py @@ -0,0 +1,6 @@ +from p19_a import pair19_a + +def pair19_b(n): + if n <= 0: + return 0 + return pair19_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p20_a.py b/tests/fixtures/complexity_pass_cycle_order/p20_a.py new file mode 100644 index 000000000..2097d17e3 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p20_a.py @@ -0,0 +1,6 @@ +from p20_b import pair20_b + +def pair20_a(n): + for _ in range(n): + pass + return pair20_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p20_b.py b/tests/fixtures/complexity_pass_cycle_order/p20_b.py new file mode 100644 index 000000000..fc0535bdf --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p20_b.py @@ -0,0 +1,6 @@ +from p20_a import pair20_a + +def pair20_b(n): + if n <= 0: + return 0 + return pair20_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p21_a.py b/tests/fixtures/complexity_pass_cycle_order/p21_a.py new file mode 100644 index 000000000..47991e4c8 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p21_a.py @@ -0,0 +1,6 @@ +from p21_b import pair21_b + +def pair21_a(n): + for _ in range(n): + pass + return pair21_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p21_b.py b/tests/fixtures/complexity_pass_cycle_order/p21_b.py new file mode 100644 index 000000000..3fb5ae276 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p21_b.py @@ -0,0 +1,6 @@ +from p21_a import pair21_a + +def pair21_b(n): + if n <= 0: + return 0 + return pair21_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p22_a.py b/tests/fixtures/complexity_pass_cycle_order/p22_a.py new file mode 100644 index 000000000..e0801bcde --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p22_a.py @@ -0,0 +1,6 @@ +from p22_b import pair22_b + +def pair22_a(n): + for _ in range(n): + pass + return pair22_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p22_b.py b/tests/fixtures/complexity_pass_cycle_order/p22_b.py new file mode 100644 index 000000000..f6f47b3e2 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p22_b.py @@ -0,0 +1,6 @@ +from p22_a import pair22_a + +def pair22_b(n): + if n <= 0: + return 0 + return pair22_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/p23_a.py b/tests/fixtures/complexity_pass_cycle_order/p23_a.py new file mode 100644 index 000000000..52a3b8c73 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p23_a.py @@ -0,0 +1,6 @@ +from p23_b import pair23_b + +def pair23_a(n): + for _ in range(n): + pass + return pair23_b(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/p23_b.py b/tests/fixtures/complexity_pass_cycle_order/p23_b.py new file mode 100644 index 000000000..c895395b7 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/p23_b.py @@ -0,0 +1,6 @@ +from p23_a import pair23_a + +def pair23_b(n): + if n <= 0: + return 0 + return pair23_a(n) diff --git a/tests/fixtures/complexity_pass_cycle_order/t00_x.py b/tests/fixtures/complexity_pass_cycle_order/t00_x.py new file mode 100644 index 000000000..a67153c33 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t00_x.py @@ -0,0 +1,6 @@ +from t00_y import tri00_y + +def tri00_x(n): + for _ in range(n): + pass + return tri00_y(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t00_y.py b/tests/fixtures/complexity_pass_cycle_order/t00_y.py new file mode 100644 index 000000000..01b6464bd --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t00_y.py @@ -0,0 +1,6 @@ +from t00_z import tri00_z + +def tri00_y(n): + for _ in range(n): + pass + return tri00_z(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t00_z.py b/tests/fixtures/complexity_pass_cycle_order/t00_z.py new file mode 100644 index 000000000..5584e2b49 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t00_z.py @@ -0,0 +1,6 @@ +from t00_x import tri00_x + +def tri00_z(n): + for _ in range(n): + pass + return tri00_x(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t01_x.py b/tests/fixtures/complexity_pass_cycle_order/t01_x.py new file mode 100644 index 000000000..c4a2a8cf5 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t01_x.py @@ -0,0 +1,6 @@ +from t01_y import tri01_y + +def tri01_x(n): + for _ in range(n): + pass + return tri01_y(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t01_y.py b/tests/fixtures/complexity_pass_cycle_order/t01_y.py new file mode 100644 index 000000000..e1f0361a9 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t01_y.py @@ -0,0 +1,6 @@ +from t01_z import tri01_z + +def tri01_y(n): + for _ in range(n): + pass + return tri01_z(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t01_z.py b/tests/fixtures/complexity_pass_cycle_order/t01_z.py new file mode 100644 index 000000000..7998c851e --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t01_z.py @@ -0,0 +1,6 @@ +from t01_x import tri01_x + +def tri01_z(n): + for _ in range(n): + pass + return tri01_x(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t02_x.py b/tests/fixtures/complexity_pass_cycle_order/t02_x.py new file mode 100644 index 000000000..897202553 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t02_x.py @@ -0,0 +1,6 @@ +from t02_y import tri02_y + +def tri02_x(n): + for _ in range(n): + pass + return tri02_y(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t02_y.py b/tests/fixtures/complexity_pass_cycle_order/t02_y.py new file mode 100644 index 000000000..e6f78bc59 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t02_y.py @@ -0,0 +1,6 @@ +from t02_z import tri02_z + +def tri02_y(n): + for _ in range(n): + pass + return tri02_z(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t02_z.py b/tests/fixtures/complexity_pass_cycle_order/t02_z.py new file mode 100644 index 000000000..d056cf849 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t02_z.py @@ -0,0 +1,6 @@ +from t02_x import tri02_x + +def tri02_z(n): + for _ in range(n): + pass + return tri02_x(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t03_x.py b/tests/fixtures/complexity_pass_cycle_order/t03_x.py new file mode 100644 index 000000000..deb0b989e --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t03_x.py @@ -0,0 +1,6 @@ +from t03_y import tri03_y + +def tri03_x(n): + for _ in range(n): + pass + return tri03_y(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t03_y.py b/tests/fixtures/complexity_pass_cycle_order/t03_y.py new file mode 100644 index 000000000..de3a290af --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t03_y.py @@ -0,0 +1,6 @@ +from t03_z import tri03_z + +def tri03_y(n): + for _ in range(n): + pass + return tri03_z(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t03_z.py b/tests/fixtures/complexity_pass_cycle_order/t03_z.py new file mode 100644 index 000000000..0bfda4c58 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t03_z.py @@ -0,0 +1,6 @@ +from t03_x import tri03_x + +def tri03_z(n): + for _ in range(n): + pass + return tri03_x(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t04_x.py b/tests/fixtures/complexity_pass_cycle_order/t04_x.py new file mode 100644 index 000000000..51d9bf949 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t04_x.py @@ -0,0 +1,6 @@ +from t04_y import tri04_y + +def tri04_x(n): + for _ in range(n): + pass + return tri04_y(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t04_y.py b/tests/fixtures/complexity_pass_cycle_order/t04_y.py new file mode 100644 index 000000000..f785837d1 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t04_y.py @@ -0,0 +1,6 @@ +from t04_z import tri04_z + +def tri04_y(n): + for _ in range(n): + pass + return tri04_z(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t04_z.py b/tests/fixtures/complexity_pass_cycle_order/t04_z.py new file mode 100644 index 000000000..f029c5fc3 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t04_z.py @@ -0,0 +1,6 @@ +from t04_x import tri04_x + +def tri04_z(n): + for _ in range(n): + pass + return tri04_x(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t05_x.py b/tests/fixtures/complexity_pass_cycle_order/t05_x.py new file mode 100644 index 000000000..d91398497 --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t05_x.py @@ -0,0 +1,6 @@ +from t05_y import tri05_y + +def tri05_x(n): + for _ in range(n): + pass + return tri05_y(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t05_y.py b/tests/fixtures/complexity_pass_cycle_order/t05_y.py new file mode 100644 index 000000000..d4d95e83f --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t05_y.py @@ -0,0 +1,6 @@ +from t05_z import tri05_z + +def tri05_y(n): + for _ in range(n): + pass + return tri05_z(n - 1) diff --git a/tests/fixtures/complexity_pass_cycle_order/t05_z.py b/tests/fixtures/complexity_pass_cycle_order/t05_z.py new file mode 100644 index 000000000..b865893ad --- /dev/null +++ b/tests/fixtures/complexity_pass_cycle_order/t05_z.py @@ -0,0 +1,6 @@ +from t05_x import tri05_x + +def tri05_z(n): + for _ in range(n): + pass + return tri05_x(n - 1) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index cf40dd57a..ab239eeef 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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: " ". + * 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 @@ -13455,6 +13675,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_objectscript_export_aggregate_exceeds_arena_block_table); 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);