From 426676b60b52a8ffc2c1db74ed8809d572e2d16e Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 2 Oct 2026 00:53:42 +0200 Subject: [PATCH] fix(graph): typedef'd C aggregates get nodes; member access binds by object type Two defects that fed each other, fixed together because the first alone would let the new Field nodes attract bare-name calls. 1. An anonymous aggregate under a typedef had no nodes. `typedef struct { ... } X;` produced no Class node and none of its members a Field node: the struct_specifier carries no name of its own, and extract_class_def gave up on it. That is the dominant C spelling of a type (37 of the 38 structs in internal/cbm/cbm.h). The enclosing type_definition now names the aggregate through its declarator; only a plain type_identifier declarator counts, since a pointer, array or function typedef names another type. Applies to struct, union and enum specifiers in C and C++ (`.h` is classified C++). 2. A C member access was resolved by its bare member name. The member half of `a.b` / `a->b` reached the short-name registry as `b`, which returned whichever Field (or function, or variable) was called `b`: on this repository 9,150 USAGE and 2,810 WRITES edges onto Fields from C sources, 3,976 of the USAGE edges onto a name several structs share. The C LSP already types the object of a field_expression. It now publishes the owner as a FIELD_REFERENCE row, one per (enclosing function, object type, field): the pipeline edge is function -> Field, so a row per occurrence would only add a record per member access. The usage and read/write resolvers (sequential and parallel) join those rows by function and member name; a C member name with no typed owner binds nothing. Four things had to hold for the owner to be found: - the cross-file registry folded Field defs into their owning type for Go only, so every C/C++ struct from another file (every header) registered with zero fields. The fold now covers C and C++; - c_register_lsp_defs kept the first 63 fields of a type. It is sized from the list now: a cap there silently dropped edges onto later fields; - a struct is registered under the header that declares it while the use site knows its short name. The owner type resolves as written, then module-qualified, then by short name from this module, a directly included header, or as the single candidate. Two same-named structs with no evidence between them resolve to nothing; - the parallel resolver ran the cross-file pass only for files with unqualified call sites. A C member access or member call that still has no owner now requests it too. Calls landing on a Field follow the same rule (cbm_call_onto_field_policy): dropped across languages (the #1928 reference rule; a Bash `command` or a Python `sorted()` is not a C struct member), dropped for a bare C call (`socket(...)`, a function-pointer parameter), and bound by owner for a C or C++ member call. Bare C++ calls and every other language are unchanged. Measured on this repository, old binary vs new: C/H Class nodes 155 -> 1000 C/H Field nodes 1047 -> 5478 enumerators of formerly anonymous enums +911 Variable nodes USAGE onto the new types, enums and constants +23k edges USAGE -> Field from .c, owner type named in the file 58.1% -> 97.6% USAGE onto a non-Field, name only used as a member 146 -> 0 WRITES -> Field from .c 2981 -> 6534 CALLS 159 removed, 3 added index wall time unchanged within noise; peak footprint +2.8% full macOS suite: 8309 passed, 0 failed, 10 skipped Tests: extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef, pipeline_c_member_access_binds_field_by_object_type (+ _parallel), call_onto_field_follows_language_and_shape, c_member_access_binds_by_owner_only. Each was RED on the unchanged tree; the pipeline tests are RED again with the call policy neutralized. Not in this change: a function-pointer member declared inline (`void (*f)(int);`) has no Field node; a `p->f()` call is not resolved (the registry splits on `.` and `::` only); inline functions in `.h` files keep the bare-name member binding; designated initializers are not typed. Signed-off-by: Martin Vogel --- internal/cbm/cbm.h | 4 + internal/cbm/extract_defs.c | 21 +++ internal/cbm/lsp/c_lsp.c | 282 ++++++++++++++++++++++++++++------ internal/cbm/lsp/c_lsp.h | 9 ++ src/pipeline/lsp_resolve.h | 161 +++++++++++++++++++ src/pipeline/pass_calls.c | 20 +++ src/pipeline/pass_lsp_cross.c | 28 ++-- src/pipeline/pass_parallel.c | 136 +++++++++++++++- src/pipeline/pass_usages.c | 52 +++++++ src/pipeline/pipeline.h | 17 ++ src/pipeline/registry.c | 46 ++++++ tests/test_extraction.c | 47 ++++++ tests/test_pipeline.c | 252 ++++++++++++++++++++++++++++++ tests/test_registry.c | 49 ++++++ 14 files changed, 1065 insertions(+), 59 deletions(-) diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index fb5e446a5..53d356c1c 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -418,6 +418,10 @@ typedef struct { typedef enum { CBM_RESOLVED_INVOCATION = 0, CBM_RESOLVED_CALL_REFERENCE, + /* C-family member access `a.b` / `a->b`: callee_qn is the Field `b` of the + * type of `a`. One row per (caller, field), not per occurrence, and no site + * span: the join is by enclosing function and member name. */ + CBM_RESOLVED_FIELD_REFERENCE, } CBMResolvedKind; // LSP-resolved invocation/reference: high-confidence type-aware resolution. diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index b655d06c1..d22558c4e 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -4708,6 +4708,27 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec } break; } + case CBM_LANG_C: + case CBM_LANG_CPP: { // `typedef struct { … } Name;`: the aggregate is + // anonymous and the typedef's declarator names it. + // A pointer/array/function declarator names another + // type, not the aggregate, so only a plain + // type_identifier counts. + if (strcmp(kind, "struct_specifier") != 0 && strcmp(kind, "union_specifier") != 0 && + strcmp(kind, "enum_specifier") != 0) { + break; + } + TSNode parent = ts_node_parent(node); + if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "type_definition") != 0) { + break; + } + TSNode declarator = ts_node_child_by_field_name(parent, TS_FIELD("declarator")); + if (!ts_node_is_null(declarator) && + strcmp(ts_node_type(declarator), "type_identifier") == 0) { + name_node = declarator; + } + break; + } default: break; } diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 74b97d69d..1d90bbffa 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -2651,12 +2651,14 @@ static uint64_t c_neg_memo_hash(const char *type_qn, const char *member_name) { return h ? h : 1ULL; } -static bool c_neg_memo_contains(const CLSPContext *ctx, uint64_t h) { - if (!ctx->neg_memo || ctx->neg_memo_cap == 0) +// Open-addressing uint64 hash set, 0 = empty slot. Shared by the negative-lookup +// memo and the emitted-field-reference set; both are malloc-owned by the context. +static bool c_u64_set_contains(const uint64_t *table, int cap, uint64_t h) { + if (!table || cap == 0) return false; - uint64_t mask = (uint64_t)ctx->neg_memo_cap - 1; + uint64_t mask = (uint64_t)cap - 1; for (uint64_t i = h & mask;; i = (i + 1) & mask) { - uint64_t slot = ctx->neg_memo[i]; + uint64_t slot = table[i]; if (slot == 0) return false; // empty slot → not present if (slot == h) @@ -2664,23 +2666,24 @@ static bool c_neg_memo_contains(const CLSPContext *ctx, uint64_t h) { } } -static void c_neg_memo_insert(CLSPContext *ctx, uint64_t h) { - // Lazy alloc / grow-by-rehash at 70% load. On OOM, silently disable the memo - // (correctness is unaffected — the full cascade still runs). - if (ctx->neg_memo == NULL) { +// Returns true when h was not in the set before this call. Lazy alloc / +// grow-by-rehash at 70% load. On OOM the set stays as it is and h counts as new: +// every caller is correct without the set, it only saves repeated work. +static bool c_u64_set_insert(uint64_t **table, int *cap, int *count, uint64_t h) { + if (*table == NULL) { uint64_t *nm = (uint64_t *)calloc(1024, sizeof(uint64_t)); if (!nm) - return; - ctx->neg_memo = nm; - ctx->neg_memo_cap = 1024; - ctx->neg_memo_count = 0; - } else if ((ctx->neg_memo_count + 1) * 10 >= ctx->neg_memo_cap * 7) { - int new_cap = ctx->neg_memo_cap * 2; + return true; + *table = nm; + *cap = 1024; + *count = 0; + } else if ((*count + 1) * 10 >= *cap * 7) { + int new_cap = *cap * 2; uint64_t *nm = (uint64_t *)calloc((size_t)new_cap, sizeof(uint64_t)); if (nm) { uint64_t nmask = (uint64_t)new_cap - 1; - for (int j = 0; j < ctx->neg_memo_cap; j++) { - uint64_t v = ctx->neg_memo[j]; + for (int j = 0; j < *cap; j++) { + uint64_t v = (*table)[j]; if (v == 0) continue; uint64_t k = v & nmask; @@ -2688,30 +2691,44 @@ static void c_neg_memo_insert(CLSPContext *ctx, uint64_t h) { k = (k + 1) & nmask; nm[k] = v; } - free(ctx->neg_memo); - ctx->neg_memo = nm; - ctx->neg_memo_cap = new_cap; + free(*table); + *table = nm; + *cap = new_cap; } // if calloc failed: keep the existing table (load may exceed 70%, still correct) } - uint64_t mask = (uint64_t)ctx->neg_memo_cap - 1; + uint64_t mask = (uint64_t)*cap - 1; for (uint64_t i = h & mask;; i = (i + 1) & mask) { - if (ctx->neg_memo[i] == 0) { - ctx->neg_memo[i] = h; - ctx->neg_memo_count++; - return; + if ((*table)[i] == 0) { + (*table)[i] = h; + (*count)++; + return true; } - if (ctx->neg_memo[i] == h) - return; // already recorded + if ((*table)[i] == h) + return false; // already recorded } } +static bool c_neg_memo_contains(const CLSPContext *ctx, uint64_t h) { + return c_u64_set_contains(ctx->neg_memo, ctx->neg_memo_cap, h); +} + +static void c_neg_memo_insert(CLSPContext *ctx, uint64_t h) { + // On OOM the memo is silently disabled (correctness is unaffected — the full + // cascade still runs). + (void)c_u64_set_insert(&ctx->neg_memo, &ctx->neg_memo_cap, &ctx->neg_memo_count, h); +} + +static void c_u64_set_free(uint64_t **table, int *cap, int *count) { + free(*table); + *table = NULL; + *cap = 0; + *count = 0; +} + static void c_neg_memo_free(CLSPContext *ctx) { - if (ctx->neg_memo) - free(ctx->neg_memo); - ctx->neg_memo = NULL; - ctx->neg_memo_cap = 0; - ctx->neg_memo_count = 0; + c_u64_set_free(&ctx->neg_memo, &ctx->neg_memo_cap, &ctx->neg_memo_count); + c_u64_set_free(&ctx->field_ref_seen, &ctx->field_ref_seen_cap, &ctx->field_ref_seen_count); } static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const char *type_qn, @@ -2934,6 +2951,100 @@ static const CBMType *c_lookup_field_type(CLSPContext *ctx, const char *type_qn, return NULL; } +// True when qn is `prefix` followed by a '.' segment boundary. +static bool c_qn_in_module(const char *qn, const char *prefix) { + size_t plen = prefix ? strlen(prefix) : 0; + return plen > 0 && strncmp(qn, prefix, plen) == 0 && qn[plen] == '.'; +} + +// The registered type a member access is made on: as written, then +// module-qualified, then by short name -- a C use site names `struct Alpha` +// while the struct is registered under the header that declares it. +// +// A short name several modules declare is taken from this module, else from a +// directly included header, else only when it is the single candidate. Two +// same-named structs with no evidence between them resolve to NOTHING: binding +// a field of the wrong struct is the defect this lookup exists to remove. +static const CBMRegisteredType *c_resolve_field_owner_type(CLSPContext *ctx, const char *type_qn) { + const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn); + if (!rt && ctx->module_qn) { + char sbuf[1024]; + const char *prefixed = c_build_module_prefixed(ctx, type_qn, sbuf, sizeof(sbuf)); + if (!prefixed) + prefixed = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, type_qn); + rt = cbm_registry_lookup_type(ctx->registry, prefixed); + } + if (rt || !ctx->registry) + return rt; + + const char *dot = strrchr(type_qn, '.'); + const char *shortn = dot ? dot + 1 : type_qn; + size_t slen = strlen(shortn); + const CBMRegisteredType *included = NULL; + const CBMRegisteredType *only = NULL; + bool included_ambiguous = false; + bool only_ambiguous = false; + CBMTypeShortIter it; + cbm_registry_types_by_short_name_chain(ctx->registry, shortn, &it); + int i; + while ((i = cbm_type_short_iter_next(&it)) >= 0) { + const CBMRegisteredType *cand = &it.reg->types[i]; + const char *q = cand->qualified_name; + size_t qlen = q ? strlen(q) : 0; + if (qlen <= slen + 1 || q[qlen - slen - 1] != '.' || strcmp(q + qlen - slen, shortn) != 0) + continue; + if (c_qn_in_module(q, ctx->module_qn)) + return cand; + if (only && strcmp(only->qualified_name, q) != 0) + only_ambiguous = true; + only = cand; + for (int inc = 0; inc < ctx->include_count; inc++) { + if (!c_qn_in_module(q, ctx->include_ns_qns[inc])) + continue; + if (included && strcmp(included->qualified_name, q) != 0) + included_ambiguous = true; + included = cand; + break; + } + } + if (included) + return included_ambiguous ? NULL : included; + return only_ambiguous ? NULL : only; +} + +// The registered type that DECLARES field_name, reached from type_qn through +// aliases and base classes as c_lookup_field_type walks them. A field whose +// type text did not parse still has an owner, so only the name decides. +static const CBMRegisteredType *c_lookup_field_owner(CLSPContext *ctx, const char *type_qn, + const char *field_name, int depth) { + if (!type_qn || !field_name || depth > 5) + return NULL; + + const CBMRegisteredType *rt = c_resolve_field_owner_type(ctx, type_qn); + if (!rt) + return NULL; + + if (rt->alias_of) + return c_lookup_field_owner(ctx, rt->alias_of, field_name, depth + 1); + + if (rt->field_names) { + for (int i = 0; rt->field_names[i]; i++) { + if (strcmp(rt->field_names[i], field_name) == 0) + return rt; + } + } + + if (rt->embedded_types) { + for (int i = 0; rt->embedded_types[i]; i++) { + const CBMRegisteredType *owner = + c_lookup_field_owner(ctx, rt->embedded_types[i], field_name, depth + 1); + if (owner) + return owner; + } + } + return NULL; +} + // ============================================================================ // c_process_statement: bind variables from statements // ============================================================================ @@ -3871,6 +3982,79 @@ static void c_emit_resolved_reference_at(CLSPContext *ctx, const char *callee_qn cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, resolved); } +// FNV-1a over (caller, type, field) with 0xff separators; 0 is remapped to 1 +// because it is the set's empty-slot sentinel. +static uint64_t c_field_ref_hash(const char *caller_qn, const char *type_qn, + const char *field_name) { + const char *parts[] = {caller_qn, type_qn, field_name}; + uint64_t h = 1469598103934665603ULL; + for (int part = 0; part < 3; part++) { + for (const unsigned char *p = (const unsigned char *)parts[part]; *p; p++) { + h ^= (uint64_t)*p; + h *= 1099511628211ULL; + } + h ^= (uint64_t)0xffu; + h *= 1099511628211ULL; + } + return h ? h : 1ULL; +} + +/* `a.b` / `a->b`: only the type of `a` says WHICH struct's `b` is meant. The + * usage and read/write resolvers see the member name alone, and a bare-name + * lookup of `b` returns whichever Field happens to be called `b`. Publish the + * owner here so they bind the member through its object's type. + * + * One row per (enclosing function, object type, field): the pipeline edge is + * function -> Field, so repeating the row for every occurrence would add a + * record per member access in the corpus and no edge. An object whose type is + * unknown, or whose type declares no such field, publishes nothing. */ +static void c_emit_field_reference(CLSPContext *ctx, TSNode node) { + if (!ctx->resolved_calls || !ctx->enclosing_func_qn) + return; + TSNode arg_node = ts_node_child_by_field_name(node, "argument", 8); + TSNode field_node = ts_node_child_by_field_name(node, "field", 5); + if (ts_node_is_null(arg_node) || ts_node_is_null(field_node) || + strcmp(ts_node_type(field_node), "field_identifier") != 0) + return; + char *field_name = c_node_text(ctx, field_node); + if (!field_name || !field_name[0]) + return; + + const CBMType *obj_type = c_eval_expr_type(ctx, arg_node); + if (cbm_type_is_unknown(obj_type)) + return; + bool is_arrow = false; + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc && !is_arrow; i++) { + TSNode child = ts_node_child(node, i); + is_arrow = !ts_node_is_named(child) && strcmp(ts_node_type(child), "->") == 0; + } + const char *type_qn = type_to_qn(c_simplify_type(ctx, obj_type, is_arrow)); + if (!type_qn) + return; + + /* Decided once per (function, type, field), hit or miss: the owner lookup + * below formats a module-qualified name on its way. */ + if (!c_u64_set_insert(&ctx->field_ref_seen, &ctx->field_ref_seen_cap, + &ctx->field_ref_seen_count, + c_field_ref_hash(ctx->enclosing_func_qn, type_qn, field_name))) + return; + + const CBMRegisteredType *owner = c_lookup_field_owner(ctx, type_qn, field_name, 0); + if (!owner || !owner->qualified_name) + return; + + CBMResolvedCall resolved = {0}; + resolved.caller_qn = ctx->enclosing_func_qn; + resolved.callee_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", owner->qualified_name, field_name); + resolved.strategy = "lsp_field_access"; + resolved.confidence = 0.95f; + resolved.kind = CBM_RESOLVED_FIELD_REFERENCE; + resolved.source_origin = ctx->source_origin; + if (resolved.callee_qn) + cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, resolved); +} + /* A direct identifier argument denotes a precise callable value only when its * nearest lexical binding is a tracked function pointer target, or when no * lexical value shadows one exact registered function. Conditional/composite @@ -3945,6 +4129,11 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { // Process statements for scope building c_process_statement(ctx, node); + // --- Member access: publish which type owns the field --- + if (strcmp(kind, "field_expression") == 0) { + c_emit_field_reference(ctx, node); + } + // --- Resolve call expressions --- if (strcmp(kind, "call_expression") == 0) { c_resolve_callable_argument_references(ctx, node); @@ -5881,21 +6070,30 @@ static void c_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const cha } } - // Field defs + // Field defs. Sized from the list itself: a fixed cap here would + // silently drop the later fields of a large struct, and with them + // every member-access edge onto those fields. if (d->field_defs) { + int fcap = 1; + for (const char *c = d->field_defs; *c; c++) { + if (*c == '|') + fcap++; + } + const char **fnarr = + (const char **)cbm_arena_alloc(arena, (fcap + 1) * sizeof(const char *)); + const CBMType **ftarr = + (const CBMType **)cbm_arena_alloc(arena, (fcap + 1) * sizeof(const CBMType *)); const char *fsrc = d->field_defs; - const char *fnames[64]; - const CBMType *ftypes[64]; int fcount = 0; - while (*fsrc && fcount < 63) { + while (fnarr && ftarr && *fsrc && fcount < fcap) { const char *sep = strchr(fsrc, '|'); const char *end = sep ? sep : fsrc + strlen(fsrc); char *pair = cbm_arena_strndup(arena, fsrc, end - fsrc); - char *colon = strchr(pair, ':'); + char *colon = pair ? strchr(pair, ':') : NULL; if (colon) { *colon = '\0'; - fnames[fcount] = pair; - ftypes[fcount] = c_parse_return_type_text( + fnarr[fcount] = pair; + ftarr[fcount] = c_parse_return_type_text( arena, colon + 1, d->def_module_qn ? d->def_module_qn : module_qn); fcount++; } @@ -5904,14 +6102,6 @@ static void c_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const cha fsrc = sep + 1; } if (fcount > 0) { - const char **fnarr = - (const char **)cbm_arena_alloc(arena, (fcount + 1) * sizeof(const char *)); - const CBMType **ftarr = (const CBMType **)cbm_arena_alloc( - arena, (fcount + 1) * sizeof(const CBMType *)); - for (int j = 0; j < fcount; j++) { - fnarr[j] = fnames[j]; - ftarr[j] = ftypes[j]; - } fnarr[fcount] = NULL; ftarr[fcount] = NULL; rt.field_names = fnarr; diff --git a/internal/cbm/lsp/c_lsp.h b/internal/cbm/lsp/c_lsp.h index af81d5bbd..0cff48f49 100644 --- a/internal/cbm/lsp/c_lsp.h +++ b/internal/cbm/lsp/c_lsp.h @@ -63,6 +63,15 @@ typedef struct { int neg_memo_cap; // power-of-two; 0 until first insert int neg_memo_count; // live entries (grow by rehash at 70% load) + // Field references already emitted for this file, keyed by (enclosing + // function, owner type, field). The function -> Field edge needs one row + // per pair; a row per occurrence would cost a record for every member + // access in the corpus. Same uint64 hash-set shape and ownership as + // neg_memo; freed at end of c_lsp_process_file. + uint64_t *field_ref_seen; + int field_ref_seen_cap; + int field_ref_seen_count; + // Output CBMResolvedCallArray *resolved_calls; CBMSourceOrigin source_origin; // source buffer represented by emitted occurrence spans diff --git a/src/pipeline/lsp_resolve.h b/src/pipeline/lsp_resolve.h index 666f9fd55..235f3f523 100644 --- a/src/pipeline/lsp_resolve.h +++ b/src/pipeline/lsp_resolve.h @@ -22,6 +22,7 @@ #define CBM_PIPELINE_LSP_RESOLVE_H #include "cbm.h" +#include "foundation/mem_core.h" /* field-owner index storage */ #include "graph_buffer/graph_buffer.h" #include "foundation/constants.h" @@ -882,6 +883,166 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference( return cbm_pipeline_find_lsp_reference_in_graph(arr, usage, allow_tail_match, NULL, NULL); } +/* ── C member access: bind a field through its owner ───────────────── + * + * The member half of `a.b` / `a->b` is not a name in any scope: only the type + * of `a` gives `b` a meaning. The C LSP publishes that as FIELD_REFERENCE rows, + * one per (enclosing function, owner field). The usage and read/write + * resolvers join them by enclosing function and member name, which is exactly + * the granularity of the function -> Field edge they emit. */ + +/* FIELD_REFERENCE rows sorted by (caller, member name, target). A NULL `rows` + * with a nonzero source array means the allocation failed and lookups scan the + * array instead, so no typed edge is ever lost to an index. */ +typedef struct { + const CBMResolvedCall **rows; + int count; +} cbm_pipeline_lsp_field_index_t; + +static inline bool cbm_pipeline_lsp_field_row_usable(const CBMResolvedCall *row) { + return row->kind == CBM_RESOLVED_FIELD_REFERENCE && row->caller_qn && row->callee_qn && + row->confidence >= CBM_LSP_CONFIDENCE_FLOOR; +} + +static inline int cbm_pipeline_lsp_field_key_cmp(const CBMResolvedCall *row, const char *caller_qn, + const char *member) { + int by_caller = strcmp(row->caller_qn, caller_qn); + return by_caller != 0 ? by_caller : strcmp(cbm_lsp_bare_segment(row->callee_qn), member); +} + +static inline int cbm_pipeline_lsp_field_row_cmp(const void *left_ptr, const void *right_ptr) { + const CBMResolvedCall *left = *(const CBMResolvedCall *const *)left_ptr; + const CBMResolvedCall *right = *(const CBMResolvedCall *const *)right_ptr; + int by_key = cbm_pipeline_lsp_field_key_cmp(left, right->caller_qn, + cbm_lsp_bare_segment(right->callee_qn)); + return by_key != 0 ? by_key : strcmp(left->callee_qn, right->callee_qn); +} + +/* False means only allocation failure; lookups then take the linear path. */ +static inline bool cbm_pipeline_lsp_field_index_build(const CBMResolvedCallArray *arr, + cbm_pipeline_lsp_field_index_t *index) { + index->rows = NULL; + index->count = 0; + int field_count = 0; + for (int i = 0; arr && i < arr->count; i++) { + if (cbm_pipeline_lsp_field_row_usable(&arr->items[i])) { + field_count++; + } + } + if (field_count == 0) { + return true; + } + const CBMResolvedCall **rows = (const CBMResolvedCall **)cbm_alloc( + CBM_MEM_CLASS_EXTRACT, (size_t)field_count * sizeof(*rows)); + if (!rows) { + return false; + } + int next = 0; + for (int i = 0; i < arr->count; i++) { + if (cbm_pipeline_lsp_field_row_usable(&arr->items[i])) { + rows[next++] = &arr->items[i]; + } + } + qsort(rows, (size_t)field_count, sizeof(*rows), cbm_pipeline_lsp_field_row_cmp); + index->rows = rows; + index->count = field_count; + return true; +} + +static inline void cbm_pipeline_lsp_field_index_free(cbm_pipeline_lsp_field_index_t *index) { + cbm_free(CBM_MEM_CLASS_EXTRACT, (void *)index->rows); + index->rows = NULL; + index->count = 0; +} + +/* Walks the owner rows of one (function, member name). `index` NULL selects + * the linear scan over `arr`. */ +typedef struct { + const cbm_pipeline_lsp_field_index_t *index; + const CBMResolvedCallArray *arr; + const char *caller_qn; + const char *member; + int next; +} cbm_pipeline_lsp_field_cursor_t; + +static inline cbm_pipeline_lsp_field_cursor_t cbm_pipeline_lsp_field_cursor( + const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_field_index_t *index, + const char *caller_qn, const char *member) { + cbm_pipeline_lsp_field_cursor_t cursor = {index, arr, caller_qn, member, 0}; + if (index && caller_qn && member) { + int low = 0; + int high = index->count; + while (low < high) { + int middle = low + (high - low) / 2; + if (cbm_pipeline_lsp_field_key_cmp(index->rows[middle], caller_qn, member) < 0) { + low = middle + 1; + } else { + high = middle; + } + } + cursor.next = low; + } + return cursor; +} + +static inline const CBMResolvedCall *cbm_pipeline_lsp_field_next( + cbm_pipeline_lsp_field_cursor_t *cursor) { + if (!cursor->caller_qn || !cursor->member) { + return NULL; + } + if (cursor->index) { + if (cursor->next >= cursor->index->count) { + return NULL; + } + const CBMResolvedCall *row = cursor->index->rows[cursor->next]; + if (cbm_pipeline_lsp_field_key_cmp(row, cursor->caller_qn, cursor->member) != 0) { + return NULL; + } + cursor->next++; + return row; + } + while (cursor->arr && cursor->next < cursor->arr->count) { + const CBMResolvedCall *row = &cursor->arr->items[cursor->next++]; + if (cbm_pipeline_lsp_field_row_usable(row) && + cbm_pipeline_lsp_field_key_cmp(row, cursor->caller_qn, cursor->member) == 0) { + return row; + } + } + return NULL; +} + +/* The graph node of an owner row, or NULL when the owner is not a project + * Field (a libc struct, say). Exact QN only: a name fallback here would undo + * the typing the row carries. */ +static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_field_node(const cbm_gbuf_t *gbuf, + const char *project_name, + const CBMResolvedCall *row) { + const cbm_gbuf_node_t *node = + cbm_pipeline_lsp_target_node_policy(gbuf, project_name, row->callee_qn, false, false); + return node && node->label && strcmp(node->label, "Field") == 0 ? node : NULL; +} + +/* The Field a C member call (`cb.close(ctx)`) lands on, decided by the type of + * its object: among the owner rows of (enclosing function, member name) that + * name a project Field, the one with the smallest QN, so the sequential scan + * and the sorted index agree. NULL when no owner is known. */ +static inline const cbm_gbuf_node_t *cbm_pipeline_c_member_call_field( + const CBMResolvedCallArray *arr, const cbm_pipeline_lsp_field_index_t *index, + const cbm_gbuf_t *gbuf, const char *project_name, const char *enclosing_func_qn, + const char *callee_text) { + cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor( + arr, index, enclosing_func_qn, cbm_lsp_bare_segment(callee_text)); + const cbm_gbuf_node_t *best = NULL; + const CBMResolvedCall *owner; + while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) { + const cbm_gbuf_node_t *field = cbm_pipeline_lsp_field_node(gbuf, project_name, owner); + if (field && (!best || strcmp(field->qualified_name, best->qualified_name) < 0)) { + best = field; + } + } + return best; +} + /* Resolve an LSP-emitted callee_qn to a graph-buffer node. * * Per-file LSPs sometimes emit `callee_qn` as the raw package-shaped diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 4d4d8fe03..d5e87ea53 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -873,6 +873,26 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM if (!target_node || source_node->id == target_node->id) { return 0; } + /* A call that resolved onto a struct Field: refused across languages and + * for a bare C call; a C/C++ member call takes the Field its object's type + * names instead of the one the member name happened to find. */ + switch (cbm_call_onto_field_policy(lang, call->callee_name, target_node->label, + target_node->file_path)) { + case CBM_FIELD_CALL_DROP: + return 0; + case CBM_FIELD_CALL_BY_OWNER: + target_node = + cbm_pipeline_c_member_call_field(lsp_calls, NULL, ctx->gbuf, ctx->project_name, + call->enclosing_func_qn, call->callee_name); + if (!target_node || source_node->id == target_node->id) { + return 0; + } + res.qualified_name = target_node->qualified_name; + res.strategy = "lsp_field_access"; + break; + case CBM_FIELD_CALL_KEEP: + break; + } /* #725: suffix_match is language-agnostic and will attach a Python * Store.commit() call to a JS function named commit (or a Bash main * to a Python main). Drop that weak cross-language edge. */ diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 3b74cc1eb..5c823a44d 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -462,24 +462,28 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch return 0; } -/* Go: fold per-field "Field" definitions into their owning struct's +/* Go and C: fold per-field "Field" definitions into their owning type's * field_defs. extract_defs.c emits one flat CBMDefinition per struct field * (label "Field", parent_class = owning struct QN, name = field name, * return_type = raw type text). Those rows are dropped by pxc_build_lsp_def - * (pxc_map_label excludes "Field"), so without this fold every Go struct - * registers with zero fields and field-chain calls (h.svc.Handle) can - * never resolve. Fields are always declared in the same file as their struct, - * so scanning the file's own defs covers every case. Runs inside + * (pxc_map_label excludes "Field"), so without this fold every struct + * registers with zero fields: Go field-chain calls (h.svc.Handle) can never + * resolve, and a C member access on a struct from another file (every header) + * has no owner to bind. Fields are always declared in the same file as their + * struct, so scanning the file's own defs covers every case. Runs inside * cbm_pxc_collect_all_defs — one site covers both the prebuilt-registry path - * and the per-file fallback, since both consume all_defs. */ -static void pxc_fold_go_struct_fields(CBMArena *arena, const CBMFileResult *result, CBMLSPDef *defs, - int start, int end) { + * and the per-file fallback, since both consume all_defs. + * + * type_label is the label the language's aggregate carries: "Struct" for Go, + * "Class" for a C struct or union. */ +static void pxc_fold_struct_fields(CBMArena *arena, const CBMFileResult *result, CBMLSPDef *defs, + int start, int end, const char *type_label) { if (!arena || !result || !defs || start >= end) { return; } for (int si = start; si < end; si++) { CBMLSPDef *dst = &defs[si]; - if (!dst->label || strcmp(dst->label, "Struct") != 0 || !dst->qualified_name) { + if (!dst->label || strcmp(dst->label, type_label) != 0 || !dst->qualified_name) { continue; } int count = 0; @@ -636,7 +640,11 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMArena *are } cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count); /* NULL-safe */ if (files[fi].language == CBM_LANG_GO) { - pxc_fold_go_struct_fields(arena, fr, defs, file_start, idx); + pxc_fold_struct_fields(arena, fr, defs, file_start, idx, "Struct"); + } else if (files[fi].language == CBM_LANG_C || files[fi].language == CBM_LANG_CPP) { + /* Both: a `.h` file is classified C++, and that is where C declares + * the structs its `.c` files access. */ + pxc_fold_struct_fields(arena, fr, defs, file_start, idx, "Class"); } if (files[fi].language == CBM_LANG_RUST) { for (int ii = 0; ii < fr->impl_traits.count; ii++) { diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 9df46c1af..86e725242 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2893,6 +2893,10 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB } } + /* Field-owner rows, indexed on the first C member call that needs them. */ + cbm_pipeline_lsp_field_index_t field_index = {0}; + bool field_index_built = false; + bool field_index_ready = false; for (int c = 0; c < result->calls.count; c++) { CBMCall *call = &result->calls.items[c]; if (!call->callee_name) { @@ -3137,6 +3141,31 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB } atomic_fetch_add_explicit(&rc->time_ns_rc_target, extract_now_ns() - _rc_t0, memory_order_relaxed); + /* A call that resolved onto a struct Field: refused across languages + * and for a bare C call; a C/C++ member call takes the Field its + * object's type names. Mirrors the sequential twin (pass_calls.c). */ + if (target_node && source_node->id != target_node->id) { + cbm_field_call_policy_t field_policy = cbm_call_onto_field_policy( + lang, call->callee_name, target_node->label, target_node->file_path); + if (field_policy == CBM_FIELD_CALL_DROP) { + continue; + } + if (field_policy == CBM_FIELD_CALL_BY_OWNER) { + if (!field_index_built) { + field_index_ready = + cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index); + field_index_built = true; + } + target_node = cbm_pipeline_c_member_call_field( + &result->resolved_calls, field_index_ready ? &field_index : NULL, rc->main_gbuf, + rc->project_name, call->enclosing_func_qn, call->callee_name); + if (!target_node || source_node->id == target_node->id) { + continue; + } + res.qualified_name = target_node->qualified_name; + res.strategy = "lsp_field_access"; + } + } if (target_node && source_node->id != target_node->id && cbm_suppress_cross_language_suffix_match(lang, target_node->file_path, res.strategy)) { /* #725: same guard as pass_calls.c — do not emit a suffix_match @@ -3180,6 +3209,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB cbm_ht_foreach(lsp_legacy_idx, lsp_idx_free_key, NULL); cbm_ht_free(lsp_legacy_idx); } + cbm_pipeline_lsp_field_index_free(&field_index); } /* Resolve usages for one file. */ @@ -3190,6 +3220,9 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws, cbm_pipeline_lsp_reference_index_t reference_index = {0}; bool reference_index_ready = cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index); + cbm_pipeline_lsp_field_index_t field_index = {0}; + bool field_index_ready = + cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index); for (int u = 0; u < result->usages.count; u++) { CBMUsage *usage = &result->usages.items[u]; if (!usage->ref_name) { @@ -3200,6 +3233,30 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws, if (!src) { continue; } + /* A C member name binds through the type of its object, published by + * the C LSP as field-owner rows, and through nothing else. Must mirror + * the sequential twin (pass_usages.c) exactly. */ + if (usage->kind == CBM_USAGE_VALUE && + cbm_c_member_binds_by_owner(lang == CBM_LANG_C, usage->is_member_access)) { + cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor( + &result->resolved_calls, field_index_ready ? &field_index : NULL, + usage->enclosing_func_qn, usage->ref_name); + const CBMResolvedCall *owner; + while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) { + const cbm_gbuf_node_t *field = + cbm_pipeline_lsp_field_node(rc->main_gbuf, rc->project_name, owner); + if (!field || field->id == src->id) { + continue; + } + char member_esc[CBM_SZ_256]; + cbm_json_escape(member_esc, sizeof(member_esc), usage->ref_name); + char member_props[CBM_SZ_512]; + snprintf(member_props, sizeof(member_props), "{\"callee\":\"%s\"}", member_esc); + cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, field->id, "USAGE", member_props); + ws->usages_resolved++; + } + continue; + } const cbm_gbuf_node_t *tgt = NULL; bool precise_call_reference = false; const CBMResolvedCall *semantic_reference = NULL; @@ -3278,6 +3335,7 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws, ws->usages_resolved++; } cbm_pipeline_lsp_reference_index_free(&reference_index); + cbm_pipeline_lsp_field_index_free(&field_index); } /* Resolve throws/raises for one file. */ @@ -3315,6 +3373,9 @@ static void resolve_file_throws(resolve_ctx_t *rc, resolve_worker_state_t *ws, static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result, const char *rel, const char *module_qn, const char **imp_keys, const char **imp_vals, int imp_count, CBMLanguage lang) { + cbm_pipeline_lsp_field_index_t field_index = {0}; + bool field_index_ready = + cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index); for (int r = 0; r < result->rw.count; r++) { CBMReadWrite *rw = &result->rw.items[r]; if (!rw->var_name) { @@ -3325,6 +3386,24 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi if (!src) { continue; } + /* C member read/write: same owner join as resolve_file_usages. Mirrors + * the sequential twin (pass_usages.c). */ + if (cbm_c_member_binds_by_owner(lang == CBM_LANG_C, rw->is_member_access)) { + cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor( + &result->resolved_calls, field_index_ready ? &field_index : NULL, + rw->enclosing_func_qn, rw->var_name); + const CBMResolvedCall *owner; + while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) { + const cbm_gbuf_node_t *field = + cbm_pipeline_lsp_field_node(rc->main_gbuf, rc->project_name, owner); + if (!field || field->id == src->id) { + continue; + } + cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, field->id, + rw->is_write ? "WRITES" : "READS", "{}"); + } + continue; + } cbm_resolution_t res = cbm_registry_resolve(rc->registry, rw->var_name, module_qn, imp_keys, imp_vals, imp_count); if (!res.qualified_name || res.qualified_name[0] == '\0') { @@ -3348,6 +3427,7 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi const char *etype = rw->is_write ? "WRITES" : "READS"; cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, etype, "{}"); } + cbm_pipeline_lsp_field_index_free(&field_index); } /* Resolve base_classes → INHERITS edges for one definition. */ @@ -3564,6 +3644,54 @@ static bool pp_has_pending_lsp_site(const CBMFileResult *result) { return false; } +/* A C member access binds only through a field-owner row. The per-file walk + * publishes owners for the structs its own file declares; a struct from a + * header is known only to the cross-file pass. A member access that still has + * no owner is therefore a request for that pass, exactly like an unqualified + * call site -- without it a file with no pending call would keep none of its + * edges onto header-declared fields. */ +static bool pp_has_unowned_c_member(const CBMFileResult *result, CBMLanguage lang) { + if (lang != CBM_LANG_C) { + return false; + } + cbm_pipeline_lsp_field_index_t field_index = {0}; + bool ready = cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index); + bool unowned = false; + for (int i = 0; i < result->usages.count && !unowned; i++) { + const CBMUsage *usage = &result->usages.items[i]; + if (usage->kind != CBM_USAGE_VALUE || !usage->is_member_access || !usage->ref_name) { + continue; + } + cbm_pipeline_lsp_field_cursor_t owners = + cbm_pipeline_lsp_field_cursor(&result->resolved_calls, ready ? &field_index : NULL, + usage->enclosing_func_qn, usage->ref_name); + unowned = cbm_pipeline_lsp_field_next(&owners) == NULL; + } + for (int i = 0; i < result->rw.count && !unowned; i++) { + const CBMReadWrite *rw = &result->rw.items[i]; + if (!rw->is_member_access || !rw->var_name) { + continue; + } + cbm_pipeline_lsp_field_cursor_t owners = + cbm_pipeline_lsp_field_cursor(&result->resolved_calls, ready ? &field_index : NULL, + rw->enclosing_func_qn, rw->var_name); + unowned = cbm_pipeline_lsp_field_next(&owners) == NULL; + } + for (int i = 0; i < result->calls.count && !unowned; i++) { + const CBMCall *call = &result->calls.items[i]; + if (!call->callee_name || + (!strchr(call->callee_name, '.') && !strstr(call->callee_name, "->"))) { + continue; + } + cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor( + &result->resolved_calls, ready ? &field_index : NULL, call->enclosing_func_qn, + cbm_lsp_bare_segment(call->callee_name)); + unowned = cbm_pipeline_lsp_field_next(&owners) == NULL; + } + cbm_pipeline_lsp_field_index_free(&field_index); + return unowned; +} + static void resolve_worker(int worker_id, void *ctx_ptr) { resolve_ctx_t *rc = ctx_ptr; resolve_worker_state_t *ws = &rc->workers[worker_id]; @@ -3651,11 +3779,13 @@ static void resolve_worker(int worker_id, void *ctx_ptr) { int semantic_sites = result->calls.count + call_reference_sites; int qualified_lsp_sites = pp_qualified_lsp_site_count(result); bool pending_lsp_site = pp_has_pending_lsp_site(result); + bool unowned_c_member = pp_has_unowned_c_member(result, lang); bool cross_lsp_eligible = (rc->all_defs && rc->def_count > 0 && cbm_pxc_has_cross_lsp(lang) && - (semantic_sites > 0 || pending_lsp_site) && - (jvm_cross_lsp || rust_workspace_cross_lsp || pending_lsp_site || - qualified_lsp_sites < semantic_sites) && + (((semantic_sites > 0 || pending_lsp_site) && + (jvm_cross_lsp || rust_workspace_cross_lsp || pending_lsp_site || + qualified_lsp_sites < semantic_sites)) || + unowned_c_member) && !is_generated); /* Skip files with nothing else to resolve and no cross-LSP work. */ diff --git a/src/pipeline/pass_usages.c b/src/pipeline/pass_usages.c index 9738f7198..9944952b9 100644 --- a/src/pipeline/pass_usages.c +++ b/src/pipeline/pass_usages.c @@ -151,6 +151,9 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res cbm_pipeline_lsp_reference_index_t reference_index = {0}; bool reference_index_ready = cbm_pipeline_lsp_reference_index_build(&result->resolved_calls, &reference_index); + cbm_pipeline_lsp_field_index_t field_index = {0}; + bool field_index_ready = + cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index); for (int u = 0; u < result->usages.count; u++) { CBMUsage *usage = &result->usages.items[u]; if (!usage->ref_name) { @@ -161,6 +164,31 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res continue; } + /* A C member name binds through the type of its object, published by + * the C LSP as field-owner rows, and through nothing else: the bare + * member name must never reach the short-name registry below. */ + if (usage->kind == CBM_USAGE_VALUE && + cbm_c_member_binds_by_owner(lang == CBM_LANG_C, usage->is_member_access)) { + cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor( + &result->resolved_calls, field_index_ready ? &field_index : NULL, + usage->enclosing_func_qn, usage->ref_name); + const CBMResolvedCall *owner; + while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) { + const cbm_gbuf_node_t *field = + cbm_pipeline_lsp_field_node(ctx->gbuf, ctx->project_name, owner); + if (!field || field->id == src->id) { + continue; + } + char member_esc[CBM_SZ_256]; + cbm_json_escape(member_esc, sizeof(member_esc), usage->ref_name); + char member_props[CBM_SZ_512]; + snprintf(member_props, sizeof(member_props), "{\"callee\":\"%s\"}", member_esc); + cbm_gbuf_insert_edge(ctx->gbuf, src->id, field->id, "USAGE", member_props); + resolved++; + } + continue; + } + const cbm_gbuf_node_t *tgt = NULL; bool precise_call_reference = false; const CBMResolvedCall *semantic_reference = NULL; @@ -242,6 +270,7 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res resolved++; } cbm_pipeline_lsp_reference_index_free(&reference_index); + cbm_pipeline_lsp_field_index_free(&field_index); return resolved; } @@ -284,6 +313,9 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result const char *module_qn, const char **imp_keys, const char **imp_vals, int imp_count, CBMLanguage lang) { int resolved = 0; + cbm_pipeline_lsp_field_index_t field_index = {0}; + bool field_index_ready = + cbm_pipeline_lsp_field_index_build(&result->resolved_calls, &field_index); for (int r = 0; r < result->rw.count; r++) { CBMReadWrite *rw = &result->rw.items[r]; if (!rw->var_name) { @@ -295,6 +327,25 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result continue; } + /* C member read/write: same owner join as resolve_usage_edges. */ + if (cbm_c_member_binds_by_owner(lang == CBM_LANG_C, rw->is_member_access)) { + cbm_pipeline_lsp_field_cursor_t owners = cbm_pipeline_lsp_field_cursor( + &result->resolved_calls, field_index_ready ? &field_index : NULL, + rw->enclosing_func_qn, rw->var_name); + const CBMResolvedCall *owner; + while ((owner = cbm_pipeline_lsp_field_next(&owners)) != NULL) { + const cbm_gbuf_node_t *field = + cbm_pipeline_lsp_field_node(ctx->gbuf, ctx->project_name, owner); + if (!field || field->id == src->id) { + continue; + } + cbm_gbuf_insert_edge(ctx->gbuf, src->id, field->id, + rw->is_write ? "WRITES" : "READS", "{}"); + resolved++; + } + continue; + } + cbm_resolution_t res = cbm_registry_resolve(ctx->registry, rw->var_name, module_qn, imp_keys, imp_vals, imp_count); if (!res.qualified_name || res.qualified_name[0] == '\0') { @@ -320,6 +371,7 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}"); resolved++; } + cbm_pipeline_lsp_field_index_free(&field_index); return resolved; } diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index 0ec6d7872..4d0421ad9 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -358,6 +358,23 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target * unit-tested in test_registry.c. */ bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label); +/* What to do with a call whose registry resolution landed on a Field node. + * Pure; unit-tested in test_registry.c. */ +typedef enum { + CBM_FIELD_CALL_KEEP = 0, /* not a Field target, or a language this rule leaves alone */ + CBM_FIELD_CALL_DROP, /* a Field can never be this call's target */ + CBM_FIELD_CALL_BY_OWNER, /* C/C++ member call: the object's type names the Field */ +} cbm_field_call_policy_t; +cbm_field_call_policy_t cbm_call_onto_field_policy(CBMLanguage caller_lang, const char *callee_text, + const char *target_label, + const char *target_file_path); + +/* True when a reference is the member half of a C `a.b` / `a->b`. Such a + * reference binds only through the C LSP's field-owner rows (the type of `a`), + * never through a bare-name lookup of `b`. C only. Pure; unit-tested in + * test_registry.c. */ +bool cbm_c_member_binds_by_owner(bool is_c, bool is_member_access); + /* Get the label of a qualified name, or NULL if not found. */ const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn); diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index bdf6f5944..96f84f94b 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -811,6 +811,52 @@ bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const cha return strcmp(target_label, "Field") == 0; } +cbm_field_call_policy_t cbm_call_onto_field_policy(CBMLanguage caller_lang, const char *callee_text, + const char *target_label, + const char *target_file_path) { + /* The short-name registry holds Field nodes, so a call spelled like some + * struct member can resolve onto that Field. Three shapes are decided here: + * - a caller in another language: nothing but the spelling connects a + * Bash `command` or a Python `sorted()` to a C struct member. The + * unique_name exemption of the CALLS guard (#1572) is about functions; + * for a Field the #1928 reference rule applies, with its JS/TS and + * C/C++ family exemptions; + * - a bare C call (`socket(...)`, a function-pointer parameter + * `want(x)`): a bare C name is never a struct member; + * - a C or C++ member call (`cb.close(ctx)`, `text.size()`): the type of + * the object decides which struct's member is called, never the + * member name alone. The C LSP publishes owners in both modes. + * A bare C++ call keeps its resolution (a method body reaches its own + * members without a selector), and so does every other language. */ + if (!target_label || strcmp(target_label, "Field") != 0) { + return CBM_FIELD_CALL_KEEP; + } + if (cbm_suppress_cross_language_ref(caller_lang, target_file_path)) { + return CBM_FIELD_CALL_DROP; + } + if (caller_lang != CBM_LANG_C && caller_lang != CBM_LANG_CPP) { + return CBM_FIELD_CALL_KEEP; + } + if (callee_text && (strchr(callee_text, '.') || strstr(callee_text, "->"))) { + return CBM_FIELD_CALL_BY_OWNER; + } + return caller_lang == CBM_LANG_C ? CBM_FIELD_CALL_DROP : CBM_FIELD_CALL_KEEP; +} + +bool cbm_c_member_binds_by_owner(bool is_c, bool is_member_access) { + /* The member half of `a.b` / `a->b` is not a name in any C scope: only the + * type of `a` gives `b` a meaning. Handing the bare `b` to the short-name + * registry bound whichever Field (or function, or variable) was called + * `b` -- on this repository 9,150 USAGE and 2,810 WRITES edges onto + * Fields, 3,976 of the USAGE edges on a name several structs share. Such a + * reference binds only through the C LSP's field-owner rows. + * + * C-gated like the Go rule above: a C++ or Objective-C method body + * reaches its own members without a selector, and their member accesses + * keep the existing resolution until their LSPs publish owners too. */ + return is_c && is_member_access; +} + /* ── Lifecycle ──────────────────────────────────────────────────── */ cbm_registry_t *cbm_registry_new(void) { diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 5a21fe3e2..4a6d5941d 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -1069,6 +1069,52 @@ TEST(c_struct) { PASS(); } +/* `typedef struct { … } Name;` is the dominant C spelling of a type: the + * aggregate itself is anonymous and the typedef's declarator names it. Without + * that name the struct had no Class node and none of its members a Field node + * (37 of the 38 structs in internal/cbm/cbm.h), so a member access could only + * bind some OTHER struct's same-named field. */ +TEST(extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef) { + CBMFileResult *r = extract("typedef struct {\n" + " int count;\n" + " const char *label;\n" + "} Tally;\n" + "\n" + "typedef union {\n" + " int as_int;\n" + " float as_float;\n" + "} Slot;\n" + "\n" + "typedef enum { SHADE_RED, SHADE_GREEN } Shade;\n" + "\n" + "typedef struct Named {\n" + " int id;\n" + "} Named;\n" + "\n" + "typedef struct {\n" + " int raw;\n" + "} *TallyRef;\n", + CBM_LANG_C, "t", "tally.h"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + /* RED before the fix: none of the three anonymous aggregates has a def. */ + ASSERT_TRUE(has_def(r, "Class", "Tally")); + ASSERT_TRUE(has_def_qn(r, "t.tally.Tally.count")); + ASSERT_TRUE(has_def_qn(r, "t.tally.Tally.label")); + ASSERT_TRUE(has_def(r, "Class", "Slot")); + ASSERT_TRUE(has_def_qn(r, "t.tally.Slot.as_int")); + ASSERT_TRUE(has_def_qn(r, "t.tally.Slot.as_float")); + ASSERT_TRUE(has_def_any(r, "Shade")); + /* A struct that names itself keeps exactly one def, under its own name. */ + ASSERT_EQ(count_defs_named(r, "Class", "Named"), 1); + ASSERT_TRUE(has_def_qn(r, "t.tally.Named.id")); + /* A pointer typedef names a pointer type, not the aggregate: no Class. */ + ASSERT_FALSE(has_def(r, "Class", "TallyRef")); + ASSERT_FALSE(has_def(r, "Field", "raw")); + cbm_free_result(r); + PASS(); +} + /* return_type of the first definition named `name`; NULL when there is no such * definition or it carries no return type. */ static const char *def_return_type(CBMFileResult *r, const char *name) { @@ -8934,6 +8980,7 @@ SUITE(extraction) { RUN_TEST(c_function_return_type_preserves_pointer_and_qualifier); RUN_TEST(c_function_return_type_plain_unchanged); RUN_TEST(c_struct); + RUN_TEST(extract_c_anonymous_typedef_aggregate_is_named_by_its_typedef); RUN_TEST(cpp_class); RUN_TEST(cpp_method_return_type_preserves_pointer_and_qualifier); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 03a5cad70..3e0abb38f 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -6745,6 +6745,256 @@ TEST(pipeline_go_bare_ref_never_binds_field_parallel) { PASS(); } +/* True when a node named src_name has an edge of edge_type to the Field + * `field` OWNED BY `owner` (qualified name ends in ".owner.field"). The + * name-only cross_file_edge_exists cannot tell two same-named fields apart, + * which is exactly what the C member-access probes below must do. */ +static bool field_edge_exists(cbm_store_t *s, const char *project, const char *src_name, + const char *owner, const char *field, const char *edge_type) { + char suffix[256]; + snprintf(suffix, sizeof(suffix), ".%s.%s", owner, field); + size_t suffix_len = strlen(suffix); + cbm_node_t *srcs = NULL; + cbm_node_t *tgts = NULL; + int sc = 0; + int tc = 0; + cbm_store_find_nodes_by_name(s, project, src_name, &srcs, &sc); + cbm_store_find_nodes_by_name(s, project, field, &tgts, &tc); + bool found = false; + for (int i = 0; i < sc && !found; i++) { + cbm_edge_t *edges = NULL; + int ec = 0; + cbm_store_find_edges_by_source_type(s, srcs[i].id, edge_type, &edges, &ec); + for (int j = 0; j < ec && !found; j++) { + for (int k = 0; k < tc && !found; k++) { + const char *qn = tgts[k].qualified_name; + size_t qn_len = qn ? strlen(qn) : 0; + found = edges[j].target_id == tgts[k].id && tgts[k].label && + strcmp(tgts[k].label, "Field") == 0 && qn_len >= suffix_len && + strcmp(qn + qn_len - suffix_len, suffix) == 0; + } + } + if (edges) { + cbm_store_free_edges(edges, ec); + } + } + if (srcs) { + cbm_store_free_nodes(srcs, sc); + } + if (tgts) { + cbm_store_free_nodes(tgts, tc); + } + return found; +} + +/* Number of edges of edge_type that END on the Field `field` owned by `owner`, + * whatever their source: the call probes below must hold for a caller in any + * language, including a shell script whose source node has no stable name. */ +static int field_inbound_edge_count(cbm_store_t *s, const char *project, const char *owner, + const char *field, const char *edge_type) { + char suffix[256]; + snprintf(suffix, sizeof(suffix), ".%s.%s", owner, field); + size_t suffix_len = strlen(suffix); + cbm_node_t *tgts = NULL; + int tc = 0; + cbm_store_find_nodes_by_name(s, project, field, &tgts, &tc); + int total = 0; + for (int k = 0; k < tc; k++) { + const char *qn = tgts[k].qualified_name; + size_t qn_len = qn ? strlen(qn) : 0; + if (!tgts[k].label || strcmp(tgts[k].label, "Field") != 0 || qn_len < suffix_len || + strcmp(qn + qn_len - suffix_len, suffix) != 0) { + continue; + } + cbm_edge_t *edges = NULL; + int ec = 0; + cbm_store_find_edges_by_target_type(s, tgts[k].id, edge_type, &edges, &ec); + total += ec; + if (edges) { + cbm_store_free_edges(edges, ec); + } + } + if (tgts) { + cbm_store_free_nodes(tgts, tc); + } + return total; +} + +/* Fixture for the C member-access probes: two structs share the field name + * `count`, and each function touches exactly one of them. A bare-name lookup + * of `count` can only pick one Field for both functions; only the type of the + * object expression says which struct is meant. `use_opaque` reaches `count` + * through a type the project never defines, so no Field may be bound at all. + * + * Calls follow the same rule. The structs carry a function-pointer member + * `notify` (declared through a typedef: a member spelled `void (*notify)(int)` + * has no Field node today, a separate extraction gap). `use_remote` calls + * Beta's `notify` from a file that declares its own struct with a `notify` + * member -- the candidate a bare-name lookup prefers, being in the same + * module. `call_param` calls a PARAMETER named like the field `alpha_only`, + * and `tool.sh` runs a command of that name: neither calls a struct member. */ +static void write_c_member_field_fixture(const char *tmp, int pad_files) { + write_temp_file(tmp, "alpha.h", + "#ifndef ALPHA_H\n" + "#define ALPHA_H\n" + "typedef void (*alpha_notify_fn)(int);\n" + "struct Alpha {\n" + " int count;\n" + " int alpha_only;\n" + " alpha_notify_fn notify;\n" + "};\n" + "#endif\n"); + write_temp_file(tmp, "beta.h", + "#ifndef BETA_H\n" + "#define BETA_H\n" + "typedef void (*beta_notify_fn)(int);\n" + "typedef struct {\n" + " int count;\n" + " int beta_only;\n" + " beta_notify_fn notify;\n" + "} Beta;\n" + "#endif\n"); + write_temp_file(tmp, "use_alpha.c", + "#include \"alpha.h\"\n" + "\n" + "int use_alpha(struct Alpha *a) {\n" + " a->count = 1;\n" + " return a->count + a->alpha_only;\n" + "}\n" + "\n" + "int call_param(int (*alpha_only)(void)) {\n" + " return alpha_only();\n" + "}\n"); + write_temp_file(tmp, "mix.c", + "#include \"beta.h\"\n" + "\n" + "typedef void (*local_notify_fn)(int);\n" + "struct Local {\n" + " local_notify_fn notify;\n" + "};\n" + "\n" + "void use_remote(Beta b) {\n" + " b.notify(3);\n" + "}\n"); + write_temp_file(tmp, "tool.sh", + "#!/bin/sh\n" + "alpha_only --check\n"); + write_temp_file(tmp, "use_beta.c", + "#include \"beta.h\"\n" + "\n" + "int use_beta(Beta b) {\n" + " b.count = 2;\n" + " return b.count + b.beta_only;\n" + "}\n"); + write_temp_file(tmp, "use_opaque.c", + "struct Opaque;\n" + "\n" + "int use_opaque(struct Opaque *o) {\n" + " return o->count;\n" + "}\n"); + for (int i = 0; i < pad_files; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "pad/filler%d.c", i); + snprintf(body, sizeof(body), "int filler%d(void) { return %d; }\n", i, i); + write_temp_file(tmp, name, body); + } +} + +/* Shared assertions for the sequential and the parallel twin. */ +static int assert_c_member_fields_bind_by_type(cbm_store_t *s, const char *project) { + /* The probe means nothing unless both same-named fields exist. */ + ASSERT_GTE(fixture_node_count(s, project, "alpha.h", "count", "Field"), 1); + ASSERT_GTE(fixture_node_count(s, project, "beta.h", "count", "Field"), 1); + /* Reproduce-first: RED while `count` resolves by bare name — both + * functions then bind the SAME Field, so one of each pair is wrong. */ + ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "count", "USAGE")); + ASSERT_FALSE(field_edge_exists(s, project, "use_alpha", "Beta", "count", "USAGE")); + ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "count", "USAGE")); + ASSERT_FALSE(field_edge_exists(s, project, "use_beta", "Alpha", "count", "USAGE")); + ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "count", "WRITES")); + ASSERT_FALSE(field_edge_exists(s, project, "use_alpha", "Beta", "count", "WRITES")); + ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "count", "WRITES")); + ASSERT_FALSE(field_edge_exists(s, project, "use_beta", "Alpha", "count", "WRITES")); + /* A field whose name is unique keeps its edge. */ + ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "alpha_only", "USAGE")); + ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "beta_only", "USAGE")); + /* An object whose type the project never defines binds no Field. */ + ASSERT_FALSE(field_edge_exists(s, project, "use_opaque", "Alpha", "count", "USAGE")); + ASSERT_FALSE(field_edge_exists(s, project, "use_opaque", "Beta", "count", "USAGE")); + /* A call through a function-pointer member lands on the member of its + * object's type, not on the same-named member its own file declares. */ + ASSERT_GTE(fixture_node_count(s, project, "mix.c", "notify", "Field"), 1); + ASSERT_TRUE(field_edge_exists(s, project, "use_remote", "Beta", "notify", "CALLS")); + ASSERT_FALSE(field_edge_exists(s, project, "use_remote", "Local", "notify", "CALLS")); + /* A bare C call of a parameter named `alpha_only` and a shell command of + * that name are not calls of the struct member, however unique the name. */ + ASSERT_EQ(field_inbound_edge_count(s, project, "Alpha", "alpha_only", "CALLS"), 0); + return 0; +} + +TEST(pipeline_c_member_access_binds_field_by_object_type) { + /* `x.f` / `x->f` handed only the member name `f` to the short-name + * registry, which returned whichever Field was called `f`: 9,150 USAGE + * and 2,810 WRITES edges onto Fields on this repository, 3,976 of the + * USAGE edges onto a name several structs share. Sequential-path twin. */ + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_field_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_c_member_field_fixture(tmp, 0); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/c_field.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + int rc = assert_c_member_fields_bind_by_type(s, project); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + if (rc != 0) { + return rc; + } + PASS(); +} + +TEST(pipeline_c_member_access_binds_field_by_object_type_parallel) { + /* Parallel twin: resolve_file_usages / resolve_file_rw are independent + * resolvers and must take the same typed join (#1928's lesson). */ + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_fieldp_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_c_member_field_fixture(tmp, 52); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/c_fieldp.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + int rc = assert_c_member_fields_bind_by_type(s, project); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + if (rc != 0) { + return rc; + } + PASS(); +} + /* Count nodes with the given exact name in the project (e.g. a Route path). */ static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) { cbm_node_t *ns = NULL; @@ -16264,6 +16514,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel); RUN_TEST(pipeline_go_bare_ref_never_binds_field); RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel); + RUN_TEST(pipeline_c_member_access_binds_field_by_object_type); + RUN_TEST(pipeline_c_member_access_binds_field_by_object_type_parallel); RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges); RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges); RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge); diff --git a/tests/test_registry.c b/tests/test_registry.c index f77b1460b..53895a29a 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1037,6 +1037,53 @@ TEST(go_bare_ref_never_binds_field) { PASS(); } +TEST(call_onto_field_follows_language_and_shape) { + /* A call the registry resolved onto a Field node. */ + /* Another language: only the spelling connects them. */ + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_BASH, "command", "Field", "src/probe.h"), + CBM_FIELD_CALL_DROP); + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_PYTHON, "sorted", "Field", "src/pool.c"), + CBM_FIELD_CALL_DROP); + /* A bare C call is never a struct member. */ + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_C, "socket", "Field", "tests/t.c"), + CBM_FIELD_CALL_DROP); + /* A C member call is decided by its object's type, in a .c or a .h. */ + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_C, "cb.close", "Field", "src/cb.h"), + CBM_FIELD_CALL_BY_OWNER); + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_C, "w->svc->app.cancel", "Field", "src/a.c"), + CBM_FIELD_CALL_BY_OWNER); + /* C++ reaches its own members bare, so a bare call keeps its resolution; + * a member call is typed like C's (`text.size()` is not some struct's + * `size` member). */ + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_CPP, "handler", "Field", "src/a.h"), + CBM_FIELD_CALL_KEEP); + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_CPP, "text.size", "Field", "src/a.h"), + CBM_FIELD_CALL_BY_OWNER); + /* The same language as the Field keeps its resolution too. */ + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_GO, "s.fn", "Field", "pkg/s.go"), + CBM_FIELD_CALL_KEEP); + /* Not a Field target: not this rule's business. */ + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_BASH, "main", "Function", "src/main.c"), + CBM_FIELD_CALL_KEEP); + ASSERT_EQ(cbm_call_onto_field_policy(CBM_LANG_C, "socket", NULL, "tests/t.c"), + CBM_FIELD_CALL_KEEP); + PASS(); +} + +TEST(c_member_access_binds_by_owner_only) { + /* The member half of a C `a.b` / `a->b` means nothing without the type of + * `a`: it binds through the C LSP's field-owner rows and never through a + * bare-name lookup. The selector shape arrives as is_member_access. */ + ASSERT_TRUE(cbm_c_member_binds_by_owner(true, true)); + /* A bare C identifier keeps the ordinary resolution. */ + ASSERT_FALSE(cbm_c_member_binds_by_owner(true, false)); + /* Other languages keep theirs: a C++ or Objective-C method body reaches its + * own members without a selector, and Go has its own rule above. */ + ASSERT_FALSE(cbm_c_member_binds_by_owner(false, true)); + ASSERT_FALSE(cbm_c_member_binds_by_owner(false, false)); + PASS(); +} + TEST(dynamic_suppress_drops_weak_method_matches) { /* #592/#606/#1276: a member call whose receiver the LSP could not type, that * landed via a WEAK short-name strategy, is generic-resolver noise → drop. @@ -1296,6 +1343,8 @@ SUITE(registry) { RUN_TEST(registry_tie_break_is_independent_of_registration_order); RUN_TEST(cross_language_ref_drops_go_vs_c); RUN_TEST(go_bare_ref_never_binds_field); + RUN_TEST(call_onto_field_follows_language_and_shape); + RUN_TEST(c_member_access_binds_by_owner_only); RUN_TEST(dynamic_suppress_drops_weak_method_matches); RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods); RUN_TEST(python_builtin_member_table_matches_builtin_type_methods);