Files
codebase-memory-mcp/internal/cbm/cpp_branch_views.c
Martin Vogel 42a9320200 graph: one identity per conditional definition, SPAWNS edges, no cross-language guesses
The test-impact engine selects tests by walking the graph, so it can only
be as right as the graph. Three gaps made it wrong:

One identity for a definition's mutually exclusive variants
- A definition written once per #if/#else branch, per #[cfg], per
  platform file (foo_linux.c / foo_win.c, platform directories) or per Go
  build tag is ONE node. The graph buffer keeps every variant's span in a
  `variants` node property (definition labels only), so a change in any
  variant maps to the definition.
- Extraction now sees definitions inside conditional blocks where it
  skipped them: C++/C#/ObjC class members under #if (one cursor walk,
  conditional block types per language in lang_specs), Elixir defs under
  if/unless, Fortran .F90/.F95/.F03/.F08, Rust #[cfg] twins (no more
  `name#cfg(...)` QNs that lost every call), C platform files and
  directories (non-static file-scope functions).
- Haskell: the vendored scanner reads only an #if group's first branch.
  The grammar stays upstream; every further branch is parsed as its own
  same-length view (cpp_branch_views.c) and adds what the raw parse
  missed. haskell/bytestring: no new partial parses, +27 functions,
  IMPORTS 322 -> 329, 131 definitions with variant spans.
- Incremental closure repair re-parses every file of such a definition
  when one of them changes (variant partners through DEFINES), so a
  repair equals a full index.

No cross-language name guesses
- A name-only resolution (unique_name, suffix_match, qualified_suffix,
  field_type_hint, and same_module across files that only share a path
  stem) never binds a symbol another language defines.
- Bash imports are the files a script sources: every top-level command
  was read as an import of its name (`set -e` imported "set", bound to a
  TSX method in 137 scripts here).
- This repository: cross-language CALLS 5,916 -> 5,091; the remainder are
  C/C++ pairs, builtins and same-stem module collisions (recorded).

SPAWNS edges (per language, like the HTTP client patterns)
- A call that starts a program (subprocess.run, exec.Command,
  child_process.spawn, posix_spawn, system, Process.Start, Command::new,
  ... about 30 languages, through the file's own imports) gets a SPAWNS
  edge to a Process node named after the program (literal first word, or
  <dynamic>) instead of a CALLS guess. No target resolution. Both
  resolvers ask at the same point (parity test).

The engine follows the graph
- Seeds map changed lines through every variant span; a test written
  once per #if branch is one test; when a change reaches a non-test
  `main`, the tests that spawn processes are reached too (the process
  runs main, and main reaches everything).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-06 02:44:56 +02:00

184 lines
5.5 KiB
C

/*
* cpp_branch_views.c — one parse view per #if/#else branch (see the header).
*/
#include "cpp_branch_views.h"
#include <string.h>
enum { CPP_LINE_CODE, CPP_LINE_START, CPP_LINE_ELSE, CPP_LINE_END };
enum { CPP_NEST_MAX = 64 };
bool cbm_lang_needs_cpp_branch_views(CBMLanguage lang) {
return lang == CBM_LANG_HASKELL;
}
static bool cpp_word_char(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
// Classify the line at src[pos..end): a conditional directive is a '#' in
// column 0, optional blanks, then the directive word — the same lines the
// Haskell scanner treats as conditionals.
static int cpp_line_kind(const char *src, int pos, int end) {
if (pos >= end || src[pos] != '#') {
return CPP_LINE_CODE;
}
int p = pos + 1;
while (p < end && (src[p] == ' ' || src[p] == '\t')) {
p++;
}
int w = p;
while (w < end && cpp_word_char(src[w])) {
w++;
}
static const struct {
const char *word;
int kind;
} words[] = {
{"if", CPP_LINE_START}, {"ifdef", CPP_LINE_START}, {"ifndef", CPP_LINE_START},
{"else", CPP_LINE_ELSE}, {"elif", CPP_LINE_ELSE}, {"elifdef", CPP_LINE_ELSE},
{"elifndef", CPP_LINE_ELSE}, {"endif", CPP_LINE_END},
};
size_t n = (size_t)(w - p);
for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) {
if (strlen(words[i].word) == n && memcmp(src + p, words[i].word, n) == 0) {
return words[i].kind;
}
}
return CPP_LINE_CODE;
}
static int cpp_line_end(const char *src, int pos, int len) {
const char *nl = memchr(src + pos, '\n', (size_t)(len - pos));
return nl ? (int)(nl - src) : len;
}
typedef struct {
int group; // groups are numbered in order of their #if line
int branch; // branch the current line is in
bool outer; // the enclosing context is kept
} cpp_frame_t;
typedef struct {
cpp_frame_t frames[CPP_NEST_MAX];
int depth;
int overflow; // groups nested past CPP_NEST_MAX are not tracked
int next;
} cpp_nest_t;
// Track one line. Returns the innermost tracked group when the line is one of
// its #else/#elif lines, else NULL.
static cpp_frame_t *cpp_nest_step(cpp_nest_t *n, int kind, bool active) {
switch (kind) {
case CPP_LINE_START:
if (n->depth < CPP_NEST_MAX && n->overflow == 0) {
n->frames[n->depth++] = (cpp_frame_t){n->next, 0, active};
} else {
n->overflow++;
}
n->next++;
return NULL;
case CPP_LINE_ELSE:
if (n->overflow > 0 || n->depth == 0) {
return NULL;
}
n->frames[n->depth - 1].branch++;
return &n->frames[n->depth - 1];
case CPP_LINE_END:
if (n->overflow > 0) {
n->overflow--;
} else if (n->depth > 0) {
n->depth--;
}
return NULL;
default:
return NULL;
}
}
// Branch count per group. Returns the group count, or -1 on allocation failure.
static int cpp_group_branches(CBMArena *a, const char *src, int len, int **out) {
int groups = 0;
for (int pos = 0; pos < len;) {
int end = cpp_line_end(src, pos, len);
groups += cpp_line_kind(src, pos, end) == CPP_LINE_START;
pos = end + 1;
}
*out = NULL;
if (groups == 0) {
return 0;
}
int *branches = cbm_arena_alloc(a, sizeof(int) * (size_t)groups);
if (!branches) {
return -1;
}
for (int g = 0; g < groups; g++) {
branches[g] = 1;
}
cpp_nest_t nest = {.depth = 0};
for (int pos = 0; pos < len;) {
int end = cpp_line_end(src, pos, len);
cpp_frame_t *frame = cpp_nest_step(&nest, cpp_line_kind(src, pos, end), true);
if (frame) {
branches[frame->group] = frame->branch + 1;
}
pos = end + 1;
}
*out = branches;
return groups;
}
int cbm_cpp_branch_view_count(CBMArena *a, const char *src, int len) {
int *branches = NULL;
int groups = cpp_group_branches(a, src, len, &branches);
int most = 1;
for (int g = 0; g < groups; g++) {
if (branches[g] > most) {
most = branches[g];
}
}
int views = most - 1;
return views > CBM_CPP_BRANCH_VIEWS_MAX ? CBM_CPP_BRANCH_VIEWS_MAX : views;
}
// The current line belongs to the view: every enclosing group sits in the
// branch the view keeps.
static bool cpp_view_keeps(const cpp_nest_t *n, const int *branches, int view) {
if (n->depth == 0) {
return true;
}
const cpp_frame_t *top = &n->frames[n->depth - 1];
int nb = branches[top->group];
int kept = 0;
if (nb >= 2) {
kept = view < nb - 1 ? view : nb - 1;
}
return top->outer && top->branch == kept;
}
char *cbm_cpp_branch_view(CBMArena *a, const char *src, int len, int view) {
int *branches = NULL;
int groups = cpp_group_branches(a, src, len, &branches);
char *buf = groups >= 0 ? cbm_arena_alloc(a, (size_t)len + 1) : NULL;
if (!buf) {
return NULL;
}
memcpy(buf, src, (size_t)len);
buf[len] = '\0';
cpp_nest_t nest = {.depth = 0};
for (int pos = 0; pos < len;) {
int end = cpp_line_end(src, pos, len);
bool kept = cpp_view_keeps(&nest, branches, view);
int kind = cpp_line_kind(src, pos, end);
cpp_nest_step(&nest, kind, kept);
if (kind != CPP_LINE_CODE || !kept) {
for (int i = pos; i < end; i++) {
buf[i] = buf[i] == '\r' ? '\r' : ' ';
}
}
pos = end + 1;
}
return buf;
}