diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 7ce0bf7bc..0aaae5f66 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -578,6 +578,393 @@ TSTree *cbm_parse_source(TSParser *parser, const char *source, uint32_t source_l return tree; } +/* ── Attribute macros a C-family grammar cannot read past ──────────────────── + * + * `class LLVM_ABI StringRef {` is how real C++ exports a class. Without the macro's + * definition the grammar reads a function named StringRef whose body holds the members + * as statements, so the class, its methods and its fields are all lost. The same goes + * for `bool f(int a) ATTR_A ATTR_B {` and for the return type of + * `LLVM_NODISCARD StringRef f()`. + * + * Each form below cannot be valid source unless one word expands to an attribute or to + * nothing. That word is left out of a second parse through included ranges, so every + * offset and position stays that of the real file, and a file with none of these forms + * keeps its first tree. Where nothing says which of two words is the macro, none is + * left out. */ +#define CBM_ATTR_MAX_ROUNDS 6 /* a macro hides at most one nesting level per round */ +#define CBM_ATTR_MAX_DEPTH 256 +#define CBM_ATTR_MIN_CAPS 3 + +typedef struct { + TSRange *items; + uint32_t count; + uint32_t cap; + uint32_t source_len; + const char *source; + const char **defined; /* -D names the caller configured: those keep their meaning */ + bool failed; +} CBMAttrWords; + +static void cbm_attr_range_add(CBMAttrWords *words, TSRange range) { + if (words->failed) { + return; + } + if (words->count == words->cap) { + uint32_t cap = words->cap ? words->cap * 2 : 32; + TSRange *grown = + (TSRange *)cbm_realloc(CBM_MEM_CLASS_EXTRACT, words->items, cap * sizeof(TSRange)); + if (!grown) { + words->failed = true; + return; + } + words->items = grown; + words->cap = cap; + } + words->items[words->count++] = range; +} + +static void cbm_attr_word_add(CBMAttrWords *words, TSNode word) { + uint32_t word_start = ts_node_start_byte(word); + size_t word_len = ts_node_end_byte(word) - word_start; + for (const char **define = words->defined; define && *define; define++) { + size_t name_len = strcspn(*define, "="); + if (name_len == word_len && !strncmp(*define, words->source + word_start, word_len)) { + return; + } + } + cbm_attr_range_add(words, (TSRange){ts_node_start_point(word), ts_node_end_point(word), + ts_node_start_byte(word), ts_node_end_byte(word)}); +} + +static TSPoint cbm_attr_point_at(const char *source, TSNode from, uint32_t byte) { + TSPoint point = ts_node_start_point(from); + for (uint32_t i = ts_node_start_byte(from); i < byte; i++) { + if (source[i] == '\n') { + point.row++; + point.column = 0; + } else { + point.column++; + } + } + return point; +} + +/* `win_T *curwin INIT( = NULL);` is how neovim declares every global. A parenthesis + * followed by `=` opens no parameter list and no argument list, so the word before it is a + * macro taking an initializer, and the word and its parentheses are left out together. */ +static void cbm_attr_initializer_macro(TSNode declaration, const char *source, + CBMAttrWords *words) { + uint32_t begin = ts_node_start_byte(declaration); + uint32_t end = ts_node_end_byte(declaration); + for (uint32_t i = begin; i + 1 < end; i++) { + if (source[i] != '(') { + continue; + } + uint32_t next = i + 1; + while (next < end && (source[next] == ' ' || source[next] == '\t')) { + next++; + } + if (next + 1 >= words->source_len || source[next] != '=' || source[next + 1] == '=') { + continue; + } + uint32_t word = i; + while (word > begin && + (isalnum((unsigned char)source[word - 1]) || source[word - 1] == '_')) { + word--; + } + int depth = 0; + uint32_t close = i; + for (; close < words->source_len && source[close] != ';'; close++) { + depth += source[close] == '('; + depth -= source[close] == ')'; + if (depth == 0) { + break; + } + } + if (word == i || close == words->source_len || source[close] == ';') { + continue; + } + cbm_attr_range_add(words, (TSRange){cbm_attr_point_at(source, declaration, word), + cbm_attr_point_at(source, declaration, close + 1), word, + close + 1}); + return; + } +} + +/* A macro's spelling: capitals, digits and underscores, too long to be a template + * parameter. */ +static bool cbm_attr_all_caps(const char *source, TSNode word) { + uint32_t start = ts_node_start_byte(word); + uint32_t end = ts_node_end_byte(word); + bool letter = false; + for (uint32_t i = start; i < end; i++) { + char c = source[i]; + if (c >= 'A' && c <= 'Z') { + letter = true; + } else if (c != '_' && !(c >= '0' && c <= '9')) { + return false; + } + } + return letter && end - start >= CBM_ATTR_MIN_CAPS; +} + +static bool cbm_attr_spells_builtin(const char *source, TSNode word) { + static const char *const builtins[] = {"void", "bool", "char", "short", "int", + "long", "float", "double", "signed", "unsigned", + "const", "constexpr", "auto", NULL}; + uint32_t start = ts_node_start_byte(word); + size_t length = ts_node_end_byte(word) - start; + for (int i = 0; builtins[i]; i++) { + if (strlen(builtins[i]) == length && !strncmp(source + start, builtins[i], length)) { + return true; + } + } + return false; +} + +/* The one word an ERROR node holds, or a null node. */ +static TSNode cbm_attr_error_word(TSNode error) { + TSNode none = {0}; + if (ts_node_named_child_count(error) != 1) { + return none; + } + TSNode word = ts_node_named_child(error, 0); + return strcmp(ts_node_type(word), "identifier") ? none : word; +} + +static bool cbm_attr_is_tag_specifier(const char *kind) { + return !strcmp(kind, "class_specifier") || !strcmp(kind, "struct_specifier") || + !strcmp(kind, "union_specifier"); +} + +/* `bool f(int a) ATTR_A ATTR_B {`: a prototype missing its semicolon, then a "definition" + * made of two bare words. */ +static bool cbm_attr_follows_open_prototype(TSNode definition) { + TSNode previous = ts_node_prev_named_sibling(definition); + if (ts_node_is_null(previous) || strcmp(ts_node_type(previous), "declaration")) { + return false; + } + TSNode declarator = ts_node_child_by_field_name(previous, "declarator", 10); + uint32_t count = ts_node_child_count(previous); + return !ts_node_is_null(declarator) && + !strcmp(ts_node_type(declarator), "function_declarator") && count > 0 && + ts_node_is_missing(ts_node_child(previous, count - 1)); +} + +/* A word between the type and the declarator: `TYPE word f()`. */ +static void cbm_attr_leading_word(CBMAttrWords *words, const char *source, TSNode type, + TSNode word) { + if (strcmp(ts_node_type(type), "type_identifier")) { + /* `int WINAPI f()`: a builtin type cannot be the macro. */ + cbm_attr_word_add(words, word); + } else if (cbm_attr_spells_builtin(source, word)) { + /* `Q_INVOKABLE void f()`: the grammar took the macro for the type. */ + cbm_attr_word_add(words, type); + } else if (cbm_attr_all_caps(source, type) != cbm_attr_all_caps(source, word)) { + cbm_attr_word_add(words, cbm_attr_all_caps(source, type) ? type : word); + } +} + +static void cbm_attr_check_declaration(TSNode node, const char *source, CBMAttrWords *words) { + TSNode type = ts_node_child_by_field_name(node, "type", 4); + TSNode declarator = ts_node_child_by_field_name(node, "declarator", 10); + if (ts_node_is_null(declarator)) { + return; + } + const char *declarator_kind = ts_node_type(declarator); + bool definition = !strcmp(ts_node_type(node), "function_definition"); + bool bare_declarator = !strcmp(declarator_kind, "identifier"); + if (definition && bare_declarator && !ts_node_is_null(type)) { + /* A function definition needs a function declarator, so `class ATTR Name {` is the + * only reading: the specifier's name is the macro. */ + if (cbm_attr_is_tag_specifier(ts_node_type(type))) { + TSNode name = ts_node_child_by_field_name(type, "name", 4); + if (!ts_node_is_null(name) && + ts_node_is_null(ts_node_child_by_field_name(type, "body", 4))) { + cbm_attr_word_add(words, name); + } + return; + } + if (!strcmp(ts_node_type(type), "type_identifier") && + cbm_attr_follows_open_prototype(node)) { + cbm_attr_word_add(words, type); + cbm_attr_word_add(words, declarator); + return; + } + } + /* `LUA_API int (lua_gettop) (lua_State *L);` reads as a function returning a function, which + * no valid declaration is, so the word the grammar took for the type is the macro. A macro + * that wraps the name, `STATUS DEFINE(foo)(int x)`, has the capitals on the inner word. */ + if (!ts_node_is_null(type) && !strcmp(ts_node_type(type), "type_identifier") && + !strcmp(declarator_kind, "function_declarator") && cbm_attr_all_caps(source, type)) { + TSNode inner = ts_node_child_by_field_name(declarator, "declarator", 10); + TSNode inner_name = + ts_node_is_null(inner) ? inner : ts_node_child_by_field_name(inner, "declarator", 10); + if (!ts_node_is_null(inner_name) && !strcmp(ts_node_type(inner), "function_declarator") && + !cbm_attr_all_caps(source, inner_name)) { + cbm_attr_word_add(words, type); + return; + } + } + uint32_t declarator_start = ts_node_start_byte(declarator); + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_child(node, i); + if (!ts_node_is_error(child)) { + continue; + } + /* `class ATTR Name : public Base { };`: with an empty body the grammar reads a + * variable named Base, and no declaration has a base clause but a class. */ + if (!definition && !ts_node_is_null(type) && + cbm_attr_is_tag_specifier(ts_node_type(type)) && + ts_node_is_null(ts_node_child_by_field_name(type, "body", 4)) && + ts_node_start_byte(child) < declarator_start && + memchr(source + ts_node_start_byte(child), ':', + ts_node_end_byte(child) - ts_node_start_byte(child))) { + TSNode name = ts_node_child_by_field_name(type, "name", 4); + if (!ts_node_is_null(name)) { + cbm_attr_word_add(words, name); + } + return; + } + /* `void f(int a) ATTR {`: the function is in the error and the macro is what the + * grammar kept as the declarator. */ + if (definition && bare_declarator && ts_node_named_child_count(child) == 1 && + !strcmp(ts_node_type(ts_node_named_child(child, 0)), "function_declarator")) { + cbm_attr_word_add(words, declarator); + continue; + } + TSNode word = cbm_attr_error_word(child); + if (ts_node_is_null(word)) { + continue; + } + if (ts_node_start_byte(child) >= declarator_start) { + /* `int f() const ATTR {` and `void f() override ATTR;` */ + if (!strcmp(declarator_kind, "function_declarator")) { + cbm_attr_word_add(words, word); + } + } else if (!ts_node_is_null(type)) { + cbm_attr_leading_word(words, source, type, word); + } + } +} + +static bool cbm_attr_is_container(const char *kind) { + static const char *const containers[] = {"translation_unit", + "namespace_definition", + "declaration_list", + "field_declaration_list", + "linkage_specification", + "template_declaration", + "class_specifier", + "struct_specifier", + "union_specifier", + "preproc_if", + "preproc_ifdef", + "preproc_elif", + "preproc_elifdef", + "preproc_else", + NULL}; + for (int i = 0; containers[i]; i++) { + if (!strcmp(kind, containers[i])) { + return true; + } + } + return false; +} + +/* A subtree without an error can only hold the clean `class ATTR Name {` form, and that + * form never sits inside a clean function body of real source. */ +static void cbm_attr_collect(TSNode node, const char *source, CBMAttrWords *words, int depth) { + if (depth > CBM_ATTR_MAX_DEPTH || words->failed) { + return; + } + uint32_t count = ts_node_named_child_count(node); + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_named_child(node, i); + const char *kind = ts_node_type(child); + bool declaration = !strcmp(kind, "function_definition") || !strcmp(kind, "declaration") || + !strcmp(kind, "field_declaration"); + if (declaration) { + cbm_attr_check_declaration(child, source, words); + } + if (!strcmp(kind, "declaration") && ts_node_has_error(child)) { + cbm_attr_initializer_macro(child, source, words); + } + if (ts_node_has_error(child) || cbm_attr_is_container(kind)) { + cbm_attr_collect(child, source, words, depth + 1); + } + } +} + +static int cbm_attr_range_cmp(const void *a, const void *b) { + uint32_t left = ((const TSRange *)a)->start_byte; + uint32_t right = ((const TSRange *)b)->start_byte; + return (left > right) - (left < right); +} + +/* Sets the parser to read everything except the words. The last range is open, so the + * virtual final newline of cbm_parse_source stays inside it. */ +static bool cbm_attr_exclude_words(TSParser *parser, CBMAttrWords *words) { + qsort(words->items, words->count, sizeof(TSRange), cbm_attr_range_cmp); + uint32_t unique = 0; + for (uint32_t i = 0; i < words->count; i++) { + if (!unique || words->items[i].start_byte != words->items[unique - 1].start_byte) { + words->items[unique++] = words->items[i]; + } + } + words->count = unique; + TSRange *kept = + (TSRange *)cbm_alloc(CBM_MEM_CLASS_EXTRACT, ((size_t)unique + 1) * sizeof(TSRange)); + if (!kept) { + return false; + } + uint32_t from = 0; + uint32_t kept_count = 0; + TSPoint from_point = {0, 0}; + for (uint32_t i = 0; i < unique; i++) { + /* A word at the start of the file leaves nothing before it. */ + if (words->items[i].start_byte > from) { + kept[kept_count++] = (TSRange){from_point, words->items[i].start_point, from, + words->items[i].start_byte}; + } + from = words->items[i].end_byte; + from_point = words->items[i].end_point; + } + kept[kept_count++] = (TSRange){from_point, {UINT32_MAX, UINT32_MAX}, from, UINT32_MAX}; + bool set = ts_parser_set_included_ranges(parser, kept, kept_count); + cbm_free(CBM_MEM_CLASS_EXTRACT, kept); + return set; +} + +static TSTree *cbm_parse_c_source_defined(TSParser *parser, const char *source, uint32_t source_len, + TSParseOptions opts, const char **defined) { + TSTree *tree = cbm_parse_source(parser, source, source_len, opts); + CBMAttrWords words = {.source_len = source_len, .source = source, .defined = defined}; + for (int round = 0; tree && round < CBM_ATTR_MAX_ROUNDS; round++) { + uint32_t before = words.count; + cbm_attr_collect(ts_tree_root_node(tree), source, &words, 0); + if (words.failed || words.count == before || !cbm_attr_exclude_words(parser, &words)) { + break; + } + TSTree *next = cbm_parse_source(parser, source, source_len, opts); + (void)ts_parser_set_included_ranges(parser, NULL, 0); + if (!next) { + ts_parser_reset(parser); + break; + } + ts_tree_delete(tree); + tree = next; + } + cbm_free(CBM_MEM_CLASS_EXTRACT, words.items); + return tree; +} + +TSTree *cbm_parse_c_source(TSParser *parser, const char *source, uint32_t source_len, + TSParseOptions opts) { + return cbm_parse_c_source_defined(parser, source, source_len, opts, NULL); +} + // --- Parse timeout callback --- /* Budget for the tree-sitter progress callback. The PRIMARY gate is per-thread @@ -3361,7 +3748,7 @@ static void cbm_rescue_defs_from_projection(CBMExtractCtx *raw_ctx, const TSLang TSParseOptions opts = {0}; /* The projection keeps the raw source's length and line structure, * so it is parsed under the same terminated-last-line rule (#2078). */ - tree = cbm_parse_source(parser, projected, (uint32_t)raw_ctx->source_len, opts); + tree = cbm_parse_c_source(parser, projected, (uint32_t)raw_ctx->source_len, opts); } CBMHashTable *held = tree ? cbm_ht_create(CBM_SZ_256) : NULL; if (held) { @@ -3525,7 +3912,7 @@ static void extract_cpp_branch_views(const CBMExtractCtx *raw, const TSLanguage } /* A view keeps the raw source's length and line structure, so it is * parsed under the same terminated-last-line rule (#2078). */ - TSTree *tree = cbm_parse_source(parser, view, (uint32_t)raw->source_len, opts); + TSTree *tree = cbm_parse_c_source(parser, view, (uint32_t)raw->source_len, opts); if (!tree) { return; } @@ -3973,7 +4360,11 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C bool sql_ranged = language == CBM_LANG_SQL && cbm_sql_values_exclusion_on(rel_path) && cbm_sql_values_kept_ranges(source, (uint32_t)source_len, &sql_kept) && ts_parser_set_included_ranges(parser, sql_kept.items, sql_kept.count); - TSTree *tree = cbm_parse_source_repairing(parser, language, source, (uint32_t)source_len, opts); + bool c_family = language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA; + TSTree *tree = c_family ? cbm_parse_c_source_defined(parser, source, (uint32_t)source_len, opts, + extra_defines) + : cbm_parse_source_repairing(parser, language, source, + (uint32_t)source_len, opts); if (sql_ranged) { (void)ts_parser_set_included_ranges(parser, NULL, 0); } @@ -4204,16 +4595,9 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C TSParser *pp_parser = get_thread_parser(ts_lang, language); if (pp_parser) { ts_parser_reset(pp_parser); - CBMStringInput pp_input = {expanded, (uint32_t)expanded_len, false}; - TSInput pp_ts_input = { - &pp_input, - cbm_string_read, - TSInputEncodingUTF8, - NULL, - }; TSParseOptions pp_opts = {0}; TSTree *pp_tree = - ts_parser_parse_with_options(pp_parser, NULL, pp_ts_input, pp_opts); + cbm_parse_c_source(pp_parser, expanded, (uint32_t)expanded_len, pp_opts); if (pp_tree) { TSNode pp_root = ts_tree_root_node(pp_tree); cbm_sha256_hex(expanded, (size_t)expanded_len, diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 08f9f62de..5c91e4872 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -1279,6 +1279,12 @@ TSTree *cbm_parse_source(TSParser *parser, const char *source, uint32_t source_l * same-length view of it. Offsets and lines are the source's own. */ TSTree *cbm_parse_source_repairing(TSParser *parser, CBMLanguage language, const char *source, uint32_t source_len, TSParseOptions opts); +/* As above for C-family source. A word that can only be an attribute macro is left out + * of the parse, so `class LLVM_ABI StringRef {` reads as the class. Offsets and positions + * stay those of the real file. Every C-family whole-file parse goes through here, so the + * extraction and the semantic passes read one tree shape. */ +TSTree *cbm_parse_c_source(TSParser *parser, const char *source, uint32_t source_len, + TSParseOptions opts); // Extract all data from one file. Caller must call cbm_free_result(). // source must remain valid for the duration of the call. diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index e5c5699ba..7d85771c7 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -9080,6 +9080,13 @@ static void extract_c_typedef(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = cbm_is_exported(name, ctx->language); def.docstring = extract_docstring(ctx, node, name); + /* What this name aliases, as written: a tag or another typedef name. A + * member access through the alias reaches that type's fields. */ + if (tag_kind && !ts_node_is_null(tag_node)) { + def.alias_target = cbm_node_text(a, tag_node, ctx->source); + } else if (strcmp(tk, "type_identifier") == 0) { + def.alias_target = cbm_node_text(a, type, ctx->source); + } cbm_defs_push(&ctx->result->defs, a, def); } ts_tree_cursor_delete(&cursor); diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 053c21884..ea0cc5979 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -365,7 +365,7 @@ static const char *cpp_branch_types[] = {"if_statement", "for_statement", "while_statement", "switch_statement", "case_statement", "try_statement", "catch_clause", NULL}; static const char *cpp_var_types[] = {"declaration", NULL}; -static const char *cpp_assign_types[] = {"assignment_expression", NULL}; +static const char *cpp_assign_types[] = {"assignment_expression", "update_expression", NULL}; static const char *cpp_throw_types[] = {"throw_statement", NULL}; // ==================== C# ==================== @@ -490,7 +490,7 @@ static const char *c_branch_types[] = {"if_statement", "case_statement", NULL}; static const char *c_var_types[] = {"declaration", NULL}; -static const char *c_assign_types[] = {"assignment_expression", NULL}; +static const char *c_assign_types[] = {"assignment_expression", "update_expression", NULL}; // ==================== BASH ==================== static const char *bash_func_types[] = {"function_definition", NULL}; diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 2da543323..adc702142 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -167,7 +167,12 @@ void c_lsp_init(CLSPContext *ctx, CBMArena *arena, const char *source, int sourc ctx->resolved_calls = out; ctx->source_origin = CBM_SOURCE_ORIGIN_RAW; ctx->current_scope = cbm_scope_push(arena, NULL); - cbm_resolve_types_init(&ctx->field_types, arena, 0); + /* One type fact per declaration and one proof per distinct occurrence fact, so + * the totals follow the file's own size. A cap here does not bound anything; + * it withholds every later edge of a large file, at a point that even moves + * with the checkout path's length, because every hashed byte counts as work. */ + cbm_resolve_types_init(&ctx->field_types, arena, UINT64_MAX); + cbm_resolve_proof_pool_init_project(&ctx->field_types.proofs, arena); (void)cbm_lsp_proof_standalone_context(&ctx->field_types, source, source_len, module_qn, CBM_SOURCE_ORIGIN_RAW); @@ -266,8 +271,9 @@ static TSNode c_fp_unwrap_parentheses(CLSPContext *ctx, TSNode node) { for (unsigned depth = 0; !ts_node_is_null(node) && !strcmp(ts_node_type(node), "parenthesized_expression"); depth++) { + /* Only this expression's reading is lost. The rest of the file still has + * its own scopes, which a latch here would withhold from every later site. */ if (depth == 64 || ts_node_has_error(node)) { - ctx->binding_failed = true; return (TSNode){0}; } TSNode expression = {0}; @@ -1039,6 +1045,7 @@ typedef struct CProofDeclaration { TSNode site; const CBMType *signature; bool external, c_linkage; + bool no_prototype; /* C `int f();` says nothing of the parameters */ } CProofDeclaration; typedef struct CProofBuiltinBinding { @@ -1049,6 +1056,7 @@ typedef struct CProofBuiltinBinding { typedef struct CProofInclude { const char *path; const char *module; + const char *file_qn; /* the included File, when the import facts name one */ uint32_t at; const cbm_resolve_proof_t *binding_proof; } CProofInclude; @@ -1066,6 +1074,112 @@ static cbm_resolve_result_t c_proof_missing(const char *reason) { return (cbm_resolve_result_t){.analysis = CBM_RESOLVE_ANALYSIS_UNSUPPORTED, .reason = reason}; } +/* One file this translation unit includes, directly or through other files. Its key + * is the File QN, since a header and its source share a module QN; a caller with no + * import facts has only the include table's module to go on. */ +typedef struct CTUModule { + const char *module; + uint32_t at; /* where the direct include that reaches it stands in this file */ + const cbm_resolve_proof_t *via; + bool exports_only; /* reached through a header generated from this source file */ +} CTUModule; + +static uint32_t c_tu_hash(const char *text, size_t length) { + uint32_t hash = 2166136261u; + for (size_t i = 0; i < length; i++) + hash = (hash ^ (unsigned char)text[i]) * 16777619u; + return hash; +} + +static CTUModule *c_tu_find(CLSPContext *ctx, const char *module, size_t length) { + if (!ctx->tu_cap) + return NULL; + uint32_t mask = ctx->tu_cap - 1; + for (uint32_t slot = c_tu_hash(module, length) & mask;; slot = (slot + 1) & mask) { + CTUModule *entry = &ctx->tu_modules[slot]; + if (!entry->module) + return NULL; + if (!strncmp(entry->module, module, length) && !entry->module[length]) + return entry; + } +} + +static bool c_tu_add(CLSPContext *ctx, const char *module, uint32_t at, + const cbm_resolve_proof_t *via, bool exports_only) { + if ((ctx->tu_count + 1) * 10 >= ctx->tu_cap * 7) { + uint32_t cap = ctx->tu_cap ? ctx->tu_cap * 2 : 64; + CTUModule *next = cbm_arena_calloc(ctx->arena, (size_t)cap * sizeof(*next)); + if (!next) { + ctx->binding_failed = true; + return false; + } + for (uint32_t i = 0; i < ctx->tu_cap; i++) { + const CTUModule *old = &ctx->tu_modules[i]; + if (!old->module) + continue; + uint32_t slot = c_tu_hash(old->module, strlen(old->module)) & (cap - 1); + while (next[slot].module) + slot = (slot + 1) & (cap - 1); + next[slot] = *old; + } + ctx->tu_modules = next; + ctx->tu_cap = cap; + } + uint32_t mask = ctx->tu_cap - 1; + uint32_t slot = c_tu_hash(module, strlen(module)) & mask; + while (ctx->tu_modules[slot].module) + slot = (slot + 1) & mask; + ctx->tu_modules[slot] = + (CTUModule){.module = module, .at = at, .via = via, .exports_only = exports_only}; + ctx->tu_count++; + return true; +} + +/* The unit's entry for a declaration held in this file, or with this module prefix. */ +static const CTUModule *c_tu_for_declaration(CLSPContext *ctx, const char *file_qn, const char *qn, + size_t module_len) { + return file_qn ? c_tu_find(ctx, file_qn, strlen(file_qn)) : c_tu_find(ctx, qn, module_len); +} + +static const char *c_own_file_key(const CLSPContext *ctx) { + return ctx->own_file_qn ? ctx->own_file_qn : ctx->module_qn; +} + +static bool c_is_own_declaration(const CLSPContext *ctx, const char *file_qn, const char *qn, + size_t module_len) { + if (file_qn && ctx->own_file_qn) + return !strcmp(file_qn, ctx->own_file_qn); + return ctx->module_qn && module_len == ctx->module_qn_len && + !strncmp(qn, ctx->module_qn, module_len); +} + +/* The included file holding this declaration QN, visible at this point in this file. + * Without file identities a QN may nest inside its module (`m.Outer.Inner`), so try + * each prefix. */ +static const CTUModule *c_tu_owner(CLSPContext *ctx, const char *qn, uint32_t at) { + CBMTypeQNIter iterator; + cbm_registry_types_with_qn(ctx->proof_registry, qn, &iterator); + const CBMRegisteredType *candidate; + while ((candidate = cbm_type_qn_next(&iterator, NULL))) { + if (!candidate->source_file_qn) + continue; + const CTUModule *entry = + c_tu_find(ctx, candidate->source_file_qn, strlen(candidate->source_file_qn)); + if (entry && !entry->exports_only && entry->at < at) + return entry; + } + for (const char *dot = strrchr(qn, '.'); dot; dot--) { + if (*dot == '.') { + const CTUModule *entry = c_tu_find(ctx, qn, (size_t)(dot - qn)); + if (entry && !entry->exports_only && entry->at < at) + return entry; + } + if (dot == qn) + break; + } + return NULL; +} + /* Preserve every namespace segment. In particular, inline ABI namespaces * cannot be removed from a declaration identity. */ static const char *c_proof_name(CLSPContext *ctx, const char *text) { @@ -1219,12 +1333,13 @@ static bool c_proof_c_linkage(CLSPContext *ctx, TSNode node) { } static void c_proof_collect(CLSPContext *ctx, TSNode node, const char *owner, int depth) { - if (depth > CBM_LSP_MAX_LOOKUP_DEPTH) { - ctx->binding_failed = true; + /* An #elif chain nests a level per branch. Past this depth declarations go + * uncollected, so their uses abstain; nothing else in the file is affected. */ + if (depth > 512) return; - } const char *kind = ts_node_type(node); - if (!strcmp(kind, "struct_specifier") || !strcmp(kind, "class_specifier")) { + if (!strcmp(kind, "struct_specifier") || !strcmp(kind, "class_specifier") || + !strcmp(kind, "union_specifier") || !strcmp(kind, "enum_specifier")) { TSNode name = ts_node_child_by_field_name(node, "name", 4); const char *text = ts_node_is_null(name) ? NULL : c_node_text(ctx, name); const char *qn = text ? cbm_arena_sprintf(ctx->arena, "$type:%s.%s", owner, text) : NULL; @@ -1236,6 +1351,32 @@ static void c_proof_collect(CLSPContext *ctx, TSNode node, const char *owner, in c_proof_include_site(ctx, node); return; } + if (!strcmp(kind, "preproc_def") || !strcmp(kind, "preproc_function_def")) { + TSNode name = ts_node_child_by_field_name(node, "name", 4); + const char *text = ts_node_is_null(name) ? NULL : c_node_text(ctx, name); + const char *key = text ? cbm_arena_sprintf(ctx->arena, "$macro:%s", text) : NULL; + if (key) + c_proof_add_declaration(ctx, key, ts_node_end_byte(name)); + return; + } + if (!strcmp(kind, "type_definition")) { + TSNode type = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type)) + c_proof_collect(ctx, type, owner, depth + 1); + TSNode alias = ts_node_child_by_field_name(node, "declarator", 10); + for (int hop = 0; hop < 8 && !ts_node_is_null(alias); hop++) { + if (!strcmp(ts_node_type(alias), "type_identifier")) { + const char *text = c_node_text(ctx, alias); + const char *qn = + text ? cbm_arena_sprintf(ctx->arena, "$type:%s.%s", owner, text) : NULL; + if (qn) + c_proof_add_declaration(ctx, qn, ts_node_end_byte(alias)); + break; + } + alias = ts_node_child_by_field_name(alias, "declarator", 10); + } + return; + } if (!strcmp(kind, "function_definition") || !strcmp(kind, "declaration")) { uint32_t count = 0; TSNode *kids = cbm_lsp_collect_children(ctx->arena, node, &count); @@ -1245,7 +1386,8 @@ static void c_proof_collect(CLSPContext *ctx, TSNode node, const char *owner, in } for (uint32_t i = 0; i < count; i++) { const char *child_kind = ts_node_type(kids[i]); - if (!strcmp(child_kind, "struct_specifier") || !strcmp(child_kind, "class_specifier")) { + if (!strcmp(child_kind, "struct_specifier") || !strcmp(child_kind, "class_specifier") || + !strcmp(child_kind, "union_specifier") || !strcmp(child_kind, "enum_specifier")) { c_proof_collect(ctx, kids[i], owner, depth + 1); continue; } @@ -1265,6 +1407,12 @@ static void c_proof_collect(CLSPContext *ctx, TSNode node, const char *owner, in cbm_c_free_signature(ctx->arena, ctx->source, node, ctx->cpp_mode); decl->external = !c_proof_internal_declaration(ctx, node); decl->c_linkage = c_proof_c_linkage(ctx, node); + TSNode list = ts_node_parent(name_node); + list = ts_node_is_null(list) + ? list + : ts_node_child_by_field_name(list, "parameters", 10); + decl->no_prototype = !ctx->cpp_mode && !ts_node_is_null(list) && + !ts_node_named_child_count(list); } } else { ctx->binding_failed = true; @@ -1292,10 +1440,14 @@ static void c_proof_collect(CLSPContext *ctx, TSNode node, const char *owner, in c_proof_collect(ctx, body, qn, depth + 1); return; } - /* Conditional compilation, templates, classes and local prototypes need - * additional domain proofs. They are not inferred from their text. */ + /* A branch of conditional compilation declares its names in the configurations + * that compile it, and code using them compiles only in those. Error recovery + * around a directive leaves complete declarations inside the ERROR. Templates, + * classes and local prototypes need additional domain proofs. */ if (strcmp(kind, "translation_unit") && strcmp(kind, "declaration_list") && - strcmp(kind, "linkage_specification")) + strcmp(kind, "linkage_specification") && strcmp(kind, "preproc_if") && + strcmp(kind, "preproc_ifdef") && strcmp(kind, "preproc_else") && + strcmp(kind, "preproc_elif") && strcmp(kind, "preproc_elifdef") && strcmp(kind, "ERROR")) return; TSTreeCursor cursor = ts_tree_cursor_new(node); if (ts_tree_cursor_goto_first_child(&cursor)) { @@ -1341,8 +1493,83 @@ static bool c_proof_configured_wrapper(CLSPContext *ctx, TSNode node) { * depend on. Real C nests that deep: an #ifdef inside an else-if chain puts redis * src/defrag.c past 16 levels, and treating that as a failure of the whole file * withheld every proof in it. */ +static uint32_t c_proof_first_error(CLSPContext *ctx, TSNode root); +static uint32_t c_proof_first_declaring_error(CLSPContext *ctx, TSNode body); + static void c_proof_audit_syntax(CLSPContext *ctx, TSNode root) { - ctx->proof_syntax_limit = ts_node_end_byte(root); + ctx->proof_syntax_limit = c_proof_first_error(ctx, root); +} + +/* A function's own scopes are read from its own subtree. A file-scope syntax error + * elsewhere, nearly always an #ifdef that splits a statement, says nothing about + * them: tree-sitter resumes at the next complete top-level item, and a file-scope + * declaration lost in the error can only hide a name no proof would then find. + * Within a function nothing after its first error is read. Configured test owners + * keep the whole file's first error, since they name their bodies through the + * surrounding text. */ +static void c_proof_enter_function(CLSPContext *ctx, TSNode func_node) { + if (ctx->test_definition_owners && ctx->test_definition_owners->has_test_definition_owners) + return; + /* Attribute macros between the parameters and the brace put the error in the + * header, ahead of a perfectly readable body. */ + TSNode body = ts_node_child_by_field_name(func_node, "body", 4); + ctx->proof_syntax_limit = !ts_node_is_null(body) && ts_node_has_error(body) + ? c_proof_first_declaring_error(ctx, body) + : ts_node_end_byte(func_node); +} + +/* What a syntax error can do to a later site in the same function is hide a local + * declaration, and then a global of that name would seem to be in reach. An error + * holding no type and no pair of adjacent names cannot be a declaration: it is an + * #ifdef inside an expression, or a stray token, and what follows reads normally. */ +static bool c_proof_error_may_declare(TSNode error) { + uint32_t count = ts_node_named_child_count(error); + bool previous_identifier = false; + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_named_child(error, i); + const char *kind = ts_node_type(child); + if (!strcmp(kind, "comment")) + continue; + if (!strcmp(kind, "primitive_type") || !strcmp(kind, "type_identifier") || + !strcmp(kind, "sized_type_specifier") || !strcmp(kind, "struct_specifier") || + !strcmp(kind, "union_specifier") || !strcmp(kind, "enum_specifier") || + !strcmp(kind, "declaration") || !strcmp(kind, "storage_class_specifier") || + !strcmp(kind, "type_qualifier") || !strcmp(kind, "init_declarator") || + !strcmp(kind, "pointer_declarator") || !strcmp(kind, "array_declarator")) + return true; + bool identifier = !strcmp(kind, "identifier"); + if (identifier && previous_identifier) + return true; + previous_identifier = identifier; + } + return false; +} + +static uint32_t c_proof_first_declaring_error(CLSPContext *ctx, TSNode body) { + uint32_t limit = ts_node_end_byte(body); + TSTreeCursor cursor = ts_tree_cursor_new(body); + bool walking = true; + while (walking) { + TSNode node = ts_tree_cursor_current_node(&cursor); + if (ts_node_has_error(node) && !c_proof_configured_wrapper(ctx, node)) { + if (ts_node_is_error(node)) { + if (c_proof_error_may_declare(node)) { + limit = ts_node_start_byte(node); + break; + } + } else if (ts_tree_cursor_goto_first_child(&cursor)) { + continue; + } + } + while (walking && !ts_tree_cursor_goto_next_sibling(&cursor)) + walking = ts_tree_cursor_goto_parent(&cursor); + } + ts_tree_cursor_delete(&cursor); + return limit; +} + +static uint32_t c_proof_first_error(CLSPContext *ctx, TSNode root) { + uint32_t limit = ts_node_end_byte(root); TSTreeCursor cursor = ts_tree_cursor_new(root); bool walking = true; while (walking) { @@ -1350,7 +1577,7 @@ static void c_proof_audit_syntax(CLSPContext *ctx, TSNode root) { bool faulty = ts_node_has_error(node) || ts_node_is_missing(node); if (faulty && !c_proof_configured_wrapper(ctx, node)) { if (ts_node_is_error(node) || ts_node_is_missing(node)) { - ctx->proof_syntax_limit = ts_node_start_byte(node); + limit = ts_node_start_byte(node); break; } if (ts_tree_cursor_goto_first_child(&cursor)) @@ -1360,11 +1587,105 @@ static void c_proof_audit_syntax(CLSPContext *ctx, TSNode root) { walking = ts_tree_cursor_goto_parent(&cursor); } ts_tree_cursor_delete(&cursor); + return limit; +} + +static int c_proof_include_site_compare(const void *left, const void *right) { + const CProofInclude *a = left, *b = right; + if (a->at != b->at) + return a->at < b->at ? -1 : 1; + int order = strcmp(a->path, b->path); + return order ? order : strcmp(a->module, b->module); +} + +/* Everything this file sees through its includes, each at the point the direct + * include that reaches it stands. A branch makes its includes conditional, not + * absent: a name found through one was still declared there in the configurations + * that compile the use. A caller defining a header's guard first keeps it out. */ +static void c_proof_build_translation_unit(CLSPContext *ctx) { + if (!ctx->include_count || !ctx->proof_includes) + return; + CProofInclude *direct = + cbm_arena_alloc(ctx->arena, (size_t)ctx->include_count * sizeof(*direct)); + if (!direct) { + ctx->binding_failed = true; + return; + } + memcpy(direct, ctx->proof_includes, (size_t)ctx->include_count * sizeof(*direct)); + qsort(direct, (size_t)ctx->include_count, sizeof(*direct), c_proof_include_site_compare); + size_t queue_cap = 64, head = 0, tail = 0; + const char **queue = cbm_arena_alloc(ctx->arena, queue_cap * sizeof(*queue)); + if (!queue) { + ctx->binding_failed = true; + return; + } + for (int i = 0; i < ctx->include_count; i++) { + const CProofInclude *include = &direct[i]; + if (include->at == UINT32_MAX || !include->module[0]) + continue; + const char *direct_key = include->file_qn ? include->file_qn : include->module; + const char *own_key = c_own_file_key(ctx); + const CBMCFileFacts *facts = cbm_c_file_facts(ctx->c_decls, direct_key); + if (facts && facts->guard) { + const char *key = cbm_arena_sprintf(ctx->arena, "$macro:%s", facts->guard); + if (key && c_proof_declared(ctx, key, include->at)) + continue; + } + if (c_tu_find(ctx, direct_key, strlen(direct_key)) || !strcmp(direct_key, own_key)) + continue; + if (!c_tu_add(ctx, direct_key, include->at, include->binding_proof, false)) + return; + head = tail = 0; + queue[tail++] = direct_key; + while (head < tail) { + facts = cbm_c_file_facts(ctx->c_decls, queue[head++]); + if (!facts) + continue; + for (int g = 0; g < facts->generated_count; g++) { + const char *source = facts->generated_for[g]; + if (!c_tu_find(ctx, source, strlen(source)) && strcmp(source, own_key) && + !c_tu_add(ctx, source, include->at, include->binding_proof, true)) + return; + } + for (int n = 0; n < facts->include_count; n++) { + const char *next = facts->includes[n]; + if (c_tu_find(ctx, next, strlen(next)) || !strcmp(next, own_key)) + continue; + if (!c_tu_add(ctx, next, include->at, include->binding_proof, false)) + return; + if (tail == queue_cap) { + /* Consumed entries are dead: reuse their room before growing. */ + size_t live = tail - head; + size_t cap = live * 2 >= queue_cap ? queue_cap * 2 : queue_cap; + const char **grown = cap == queue_cap + ? queue + : cbm_arena_alloc(ctx->arena, cap * sizeof(*grown)); + if (!grown) { + ctx->binding_failed = true; + return; + } + memmove(grown, queue + head, live * sizeof(*grown)); + queue = grown; + queue_cap = cap; + head = 0; + tail = live; + } + queue[tail++] = next; + } + } + } } static void c_proof_prepare(CLSPContext *ctx, TSNode root) { ctx->proof_registry = c_proof_registry_snapshot(ctx, ctx->registry, 0); - ctx->proof_preprocessor_unknown = cbm_c_preprocessor_domain_unsupported(root); + ctx->proof_preprocessor_unknown = cbm_c_preprocessor_domain_unsupported(root, ctx->source); + for (const CBMTypeRegistry *link = ctx->registry; link && !ctx->c_decls; link = link->fallback) + ctx->c_decls = link->c_decls; + ctx->own_file_qn = ctx->import_facts ? ctx->import_facts->source.scope : NULL; + const CBMCFileFacts *own_facts = cbm_c_file_facts(ctx->c_decls, c_own_file_key(ctx)); + /* A .h with no C++ syntax of its own reads as C: no overloads, no argument- + * dependent lookup, and C linkage whichever language includes it. */ + ctx->plain_header = own_facts && own_facts->plain_header && !own_facts->cpp_syntax; c_proof_audit_syntax(ctx, root); if (!ctx->proof_registry || !ctx->module_qn || ctx->binding_failed) { ctx->binding_failed = true; @@ -1401,8 +1722,10 @@ static void c_proof_prepare(CLSPContext *ctx, TSNode root) { const cbm_resolve_import_fact_t *fact = facts ? cbm_ht_get(facts, include.path) : NULL; if (fact && cbm_resolve_admit(&fact->dependency) && cbm_resolve_admit(&fact->binding) && fact->dependency.proof && fact->binding.proof && - !strcmp(fact->binding.target, include.module)) + !strcmp(fact->binding.target, include.module)) { include.binding_proof = fact->binding.proof; + include.file_qn = fact->dependency.target; + } ctx->proof_includes[i] = include; } cbm_ht_free(facts); @@ -1413,6 +1736,7 @@ static void c_proof_prepare(CLSPContext *ctx, TSNode root) { if (ctx->proof_declaration_count) qsort(ctx->proof_declarations, (size_t)ctx->proof_declaration_count, sizeof(*ctx->proof_declarations), c_proof_declaration_compare); + c_proof_build_translation_unit(ctx); } static void c_proof_record_using(CLSPContext *ctx, TSNode node) { @@ -1464,7 +1788,12 @@ static cbm_resolve_method_candidate_t c_proof_classify(const cbm_resolve_method_ : candidate->qualified_name; proof.graph_target_exact = proof.graph_target != NULL; proof.declaration_identity = candidate->declaration_identity; - proof.declaration_identity_exact = candidate->declaration_identity != NULL; + /* The #if branches of one file define a function more than once, and the graph + * holds them as one node. One file and one graph identity is one declaration. */ + if (candidate->source_file_qn && proof.graph_target) + proof.declaration_identity = + cbm_arena_sprintf(ctx->arena, "%s|%s", candidate->source_file_qn, proof.graph_target); + proof.declaration_identity_exact = proof.declaration_identity != NULL; return proof; } @@ -1520,12 +1849,9 @@ static cbm_resolve_result_t c_proof_type_import(CLSPContext *ctx, const char *qn const char *key = cbm_arena_sprintf(ctx->arena, "$type:%s", qn); if (!ctx->require_import_proofs || (key && c_proof_declared(ctx, key, at))) return result; - for (int i = 0; i < ctx->include_count; i++) { - const CProofInclude *include = &ctx->proof_includes[i]; - size_t size = strlen(include->module); - if (include->at < at && size && !strncmp(qn, include->module, size) && qn[size] == '.') - return c_proof_import_join(ctx, result, include->binding_proof, CBM_PROOF_TYPE_BIND); - } + const CTUModule *owner = c_tu_owner(ctx, qn, at); + if (owner) + return c_proof_import_join(ctx, result, owner->via, CBM_PROOF_TYPE_BIND); return c_proof_missing("c_type_import_predecessor_missing"); } @@ -1577,14 +1903,62 @@ static bool c_proof_signature_equal(const CBMType *left, const CBMType *right) { * short-name guess. Every project candidate in that indexed bucket must have * audited linkage facts; incompatible, duplicate and unknown definitions close * the domain without a target. Static definitions belong to other TUs. */ +/* Does a translation unit rooted at this file include that one? A definition whose + * own file sees the prototype was compiled against it. */ +static bool c_proof_file_reaches(CLSPContext *ctx, const char *from, const char *to) { + if (!strcmp(from, to)) + return true; + enum { REACH_LIMIT = 4096 }; + const char **seen = cbm_arena_alloc(ctx->arena, REACH_LIMIT * sizeof(*seen)); + if (!seen) + return false; + int head = 0, tail = 0; + seen[tail++] = from; + while (head < tail) { + const CBMCFileFacts *facts = cbm_c_file_facts(ctx->c_decls, seen[head++]); + for (int n = 0; facts && n < facts->include_count; n++) { + const char *next = facts->includes[n]; + if (!strcmp(next, to)) + return true; + bool known = false; + for (int k = 0; k < tail && !known; k++) + known = !strcmp(seen[k], next); + if (!known && tail < REACH_LIMIT) + seen[tail++] = next; + } + } + return false; +} + +static const char *c_proof_func_file(CLSPContext *ctx, const CBMRegisteredFunc *candidate, + const char *relative) { + if (candidate->source_file_qn) + return candidate->source_file_qn; + size_t qn = strlen(candidate->qualified_name), rel = strlen(relative); + return qn > rel + 1 ? cbm_arena_strndup(ctx->arena, candidate->qualified_name, qn - rel - 1) + : NULL; +} + +static cbm_resolve_result_t c_proof_declaration_lookup(CLSPContext *ctx, const char *spelling, + TSNode site, const char *target, + const char *decision, bool imported); + +/* `declaring` names the file holding the visible prototype, or NULL when that is + * this file. Several definitions of one external name are several programs in one + * repository. The one whose own file includes this prototype was compiled against + * it, which is what makes it the definition of this declaration. */ static cbm_resolve_result_t c_proof_external_definition(CLSPContext *ctx, const char *relative, - const CBMType *signature, bool c_linkage) { + const CBMType *signature, int arity, + bool c_linkage, bool any_linkage, + const char *declaring) { if (ctx->proof_preprocessor_unknown) return c_proof_missing("c_external_preprocessor_domain_unproven"); const char *leaf = strrchr(relative, '.'); const char *name = leaf ? leaf + 1 : relative; size_t namespace_len = leaf ? (size_t)(leaf - relative) : 0; const CBMRegisteredFunc *chosen = NULL; + const CBMRegisteredFunc *compiled_against = NULL; + int definitions = 0, compiled_count = 0; CBMFreeFuncIter iterator; cbm_registry_free_funcs_by_short_name_chain(ctx->proof_registry, name, &iterator); int index; @@ -1599,26 +1973,70 @@ static cbm_resolve_result_t c_proof_external_definition(CLSPContext *ctx, const if (candidate->source_flags & CBM_SOURCE_DECL_STATIC) continue; bool candidate_c = (candidate->source_flags & CBM_SOURCE_DECL_EXTERN_C) != 0; - if (candidate_c != c_linkage) + if (!any_linkage && candidate_c != c_linkage) continue; - if (!c_linkage && (strlen(candidate->source_namespace) != namespace_len || - strncmp(candidate->source_namespace, relative, namespace_len))) + if (!candidate_c && (strlen(candidate->source_namespace) != namespace_len || + strncmp(candidate->source_namespace, relative, namespace_len))) continue; - if (!signature || !candidate->source_signature || - !c_proof_signature_equal(signature, candidate->source_signature)) + /* C identifies an external name by spelling. Signatures are compared to + * reject what a compiler would reject, as far as each is understood: + * builtin types exactly, and otherwise the parameter count. */ + if (signature && candidate->source_signature) { + if (!c_proof_signature_equal(signature, candidate->source_signature)) + return c_proof_missing("c_external_signature_conflict"); + } else if (arity >= 0 && candidate->source_arity && candidate->source_arity != arity + 1) { return c_proof_missing("c_external_signature_conflict"); + } if (!(candidate->source_flags & CBM_SOURCE_FUNCTION_DEFINITION)) continue; if (!candidate->declaration_identity || !candidate->graph_qualified_name || candidate->type_param_names) return c_proof_missing("c_external_definition_identity_missing"); - if (chosen && (strcmp(chosen->declaration_identity, candidate->declaration_identity) || - strcmp(chosen->graph_qualified_name, candidate->graph_qualified_name))) + /* #if variants of one definition share one graph node. */ + if (chosen && !strcmp(chosen->graph_qualified_name, candidate->graph_qualified_name)) + continue; + definitions++; + if (declaring && definitions <= 64) { + const char *file = c_proof_func_file(ctx, candidate, relative); + if (file && c_proof_file_reaches(ctx, file, declaring)) { + compiled_against = candidate; + compiled_count++; + } + } + chosen = candidate; + } + /* `int llvm::f(int x) { ... }` is written with its namespace in the declarator. */ + if (!chosen && namespace_len && !c_linkage) { + const char *qualified = NULL; + size_t relative_len = strlen(relative); + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + size_t target_len = decl->target ? strlen(decl->target) : 0; + if (decl->kind != CBM_C_DECL_METHOD || strcmp(decl->name, name) || + target_len <= relative_len || decl->target[target_len - relative_len - 1] != '.' || + strcmp(decl->target + target_len - relative_len, relative)) + continue; + if (arity >= 0 && decl->arity >= 0 && decl->arity != arity) + continue; + if (qualified && strcmp(qualified, decl->target)) + return (cbm_resolve_result_t){.binding = CBM_RESOLVE_BINDING_AMBIGUOUS, + .analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, + .candidate_count = 2, + .reason = "c_external_definitions_conflict"}; + qualified = decl->target; + } + if (qualified) + return c_proof_declaration_lookup(ctx, relative, (TSNode){0}, qualified, + "qualified-namespace-definition", false); + } + if (definitions > 1) { + if (compiled_count != 1) return (cbm_resolve_result_t){.binding = CBM_RESOLVE_BINDING_AMBIGUOUS, .analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, .candidate_count = 2, .reason = "c_external_definitions_conflict"}; - chosen = candidate; + chosen = compiled_against; } return chosen ? c_proof_query(ctx, chosen->qualified_name, CBM_RESOLVE_RULE_SCOPE_BINDING) : (cbm_resolve_result_t){.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, @@ -1644,17 +2062,20 @@ static cbm_resolve_result_t c_proof_visible_prototype(CLSPContext *ctx, const ch const CProofDeclaration *decl = &ctx->proof_declarations[i]; if (decl->at >= at || ts_node_is_null(decl->site)) continue; - if (!decl->external || !decl->signature || - (signature && - (!c_proof_signature_equal(signature, decl->signature) || linkage != decl->c_linkage))) + if (!decl->external || decl->no_prototype || + (!ts_node_is_null(site) && ((signature && decl->signature && + !c_proof_signature_equal(signature, decl->signature)) || + linkage != decl->c_linkage))) return c_proof_missing("c_prototype_linkage_unsupported"); - signature = decl->signature; + if (decl->signature) + signature = decl->signature; linkage = decl->c_linkage; site = decl->site; } - if (!signature) + if (ts_node_is_null(site)) return result; - result = c_proof_external_definition(ctx, relative, signature, linkage); + result = c_proof_external_definition(ctx, relative, signature, -1, linkage, + ctx->plain_header && !linkage, c_own_file_key(ctx)); return cbm_resolve_admit(&result) ? cbm_lsp_proof_rule( &ctx->field_types, result, site, "c-external-prototype", relative, @@ -1684,6 +2105,15 @@ static cbm_resolve_result_t c_proof_domain(CLSPContext *ctx, const char *relativ local_result = c_proof_visible_prototype(ctx, local, relative, at); if (!c_proof_merge(&result, local_result)) return result; + } else { + /* A use before this file's own top-level definition compiled against an + * earlier declaration, written in some header if not here. File scope has + * one meaning for an ordinary name, so that declaration is of this function. */ + const CBMRegisteredFunc *registered = cbm_registry_lookup_func(ctx->proof_registry, local); + uint32_t own = CBM_SOURCE_DECL_TOP_LEVEL | CBM_SOURCE_FUNCTION_DEFINITION; + if (registered && (registered->source_flags & own) == own && + !c_proof_merge(&result, c_proof_query(ctx, local, CBM_RESOLVE_RULE_SCOPE_BINDING))) + return result; } for (int i = 0; i < ctx->include_count; i++) { const CProofInclude *include = &ctx->proof_includes[i]; @@ -1695,16 +2125,100 @@ static cbm_resolve_result_t c_proof_domain(CLSPContext *ctx, const char *relativ !strcmp(include->path, ctx->proof_includes[i + 1].path) && strcmp(include->module, ctx->proof_includes[i + 1].module))) return c_proof_missing("c_include_mapping_conflict"); - const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", include->module, relative); + } + /* What an included file defines: one candidate per project function of this + * name, kept when its file is in this translation unit. */ + const char *leaf = strrchr(relative, '.'); + leaf = leaf ? leaf + 1 : relative; + size_t relative_len = strlen(relative); + CBMFreeFuncIter iterator; + cbm_registry_free_funcs_by_short_name_chain(ctx->proof_registry, leaf, &iterator); + int index; + while ((index = cbm_free_func_iter_next(&iterator)) >= 0) { + const CBMRegisteredFunc *candidate = &iterator.reg->funcs[index]; + const char *qn = candidate->qualified_name; + size_t qn_len = qn ? strlen(qn) : 0; + if (qn_len <= relative_len + 1 || qn[qn_len - relative_len - 1] != '.' || + strcmp(qn + qn_len - relative_len, relative)) + continue; + const CTUModule *entry = + c_tu_for_declaration(ctx, candidate->source_file_qn, qn, qn_len - relative_len - 1); + if (!entry || entry->at >= at) + continue; + if (entry->exports_only && (!(candidate->source_flags & CBM_SOURCE_FUNCTION_DEFINITION) || + (candidate->source_flags & CBM_SOURCE_DECL_STATIC))) + continue; cbm_resolve_result_t imported = c_proof_query(ctx, qn, CBM_RESOLVE_RULE_IMPORT_BINDING); - imported = - c_proof_import_join(ctx, imported, include->binding_proof, CBM_PROOF_IMPORT_BINDING); + imported = c_proof_import_join(ctx, imported, entry->via, CBM_PROOF_IMPORT_BINDING); if (!c_proof_merge(&result, imported)) return result; } + /* What an included file only declares. The call is to the external name, so + * the edge goes to the definition with that name, wherever it is compiled. */ + const CBMCDecl *declared = NULL; + const CTUModule *declared_via = NULL; + const CBMType *signature = NULL; + int arity = -1; + bool c_linkage = !ctx->cpp_mode; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, relative); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_PROTOTYPE || !(decl->flags & CBM_C_DECL_EXTERNAL) || + strcmp(decl->name, relative)) + continue; + const CTUModule *entry = c_tu_find(ctx, decl->file, strlen(decl->file)); + if (!entry || entry->exports_only || entry->at >= at) + continue; + if (signature && decl->signature && !c_proof_signature_equal(signature, decl->signature)) + return c_proof_missing("c_prototype_linkage_unsupported"); + if (decl->signature) + signature = decl->signature; + if (decl->arity >= 0) + arity = decl->arity; + if (decl->flags & CBM_C_DECL_C_LINKAGE) + c_linkage = true; + declared = decl; + declared_via = entry; + } + if (declared) { + cbm_resolve_result_t external = + c_proof_external_definition(ctx, relative, signature, arity, c_linkage, + ctx->plain_header && !c_linkage, declared->file); + if (cbm_resolve_admit(&external)) { + external = + c_proof_import_join(ctx, external, declared_via->via, CBM_PROOF_IMPORT_BINDING); + if (cbm_resolve_admit(&external)) { + TSNode none = {0}; + external = cbm_lsp_proof_rule( + &ctx->field_types, external, none, "c-header-prototype", relative, + "visible-header-prototype;compatible-signature;unique-external-definition", + NULL); + } + } + if (!c_proof_merge(&result, external)) + return result; + } return result; } +/* A macro of this name may rewrite any use, and which definition wins depends on + * the configuration. No function of the name is proven at such a use. */ +static bool c_proof_macro_visible(CLSPContext *ctx, const char *name, uint32_t at) { + const char *key = cbm_arena_sprintf(ctx->arena, "$macro:%s", name); + if (!key || c_proof_declared(ctx, key, at)) + return true; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_MACRO || strcmp(decl->name, name)) + continue; + const CTUModule *entry = c_tu_find(ctx, decl->file, strlen(decl->file)); + if (entry && !entry->exports_only && entry->at < at) + return true; + } + return false; +} + static bool c_proof_using_visible(CLSPContext *ctx, const CProofUsing *using, uint32_t at) { if (using->at >= at) return false; @@ -1720,7 +2234,8 @@ static bool c_proof_using_visible(CLSPContext *ctx, const CProofUsing *using, ui static cbm_resolve_result_t c_proof_name_lookup(CLSPContext *ctx, const char *text, uint32_t at) { const char *name = c_proof_name(ctx, text); - if (!name || ctx->binding_failed || ctx->in_template || ctx->enclosing_class_qn) + if (!name || ctx->binding_failed || ctx->in_template || + ((ctx->enclosing_class_qn || ctx->proof_method_class) && !ctx->proof_class_scope_checked)) return c_proof_missing("c_name_domain_unsupported"); if (strchr(name, '.')) { if (ctx->current_namespace && !(text[0] == ':' && text[1] == ':')) @@ -1737,6 +2252,8 @@ static cbm_resolve_result_t c_proof_name_lookup(CLSPContext *ctx, const char *te } if (cbm_scope_contains(ctx->current_scope, name)) return c_proof_missing("c_name_shadowed"); + if (c_proof_macro_visible(ctx, name, at)) + return c_proof_missing("c_macro_may_rewrite_name"); cbm_resolve_result_t result = {.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE}; const char *relative = ctx->current_namespace && @@ -1847,25 +2364,80 @@ static const cbm_resolve_proof_t *c_exact_function_binding_from_expr(CLSPContext * are enumerated, only arguments with no associated user-defined type are * complete. Qualified calls and lexical function pointers do not use ADL. */ static bool c_proof_builtin_argument(CLSPContext *ctx, TSNode argument); +static const CBMRegisteredType *c_proof_expression_owner(CLSPContext *ctx, TSNode node, int depth, + const cbm_resolve_proof_t **proof, + const CBMType **shape); + +/* Does this unit declare another function of this written name? Argument-dependent + * lookup adds only declarations the unit can see, so without one it adds nothing. */ +static bool c_proof_other_visible_function(CLSPContext *ctx, const char *name, const char *target, + uint32_t at) { + size_t name_len = strlen(name); + CBMFreeFuncIter iterator; + cbm_registry_free_funcs_by_short_name_chain(ctx->proof_registry, name, &iterator); + int index; + while ((index = cbm_free_func_iter_next(&iterator)) >= 0) { + const CBMRegisteredFunc *candidate = &iterator.reg->funcs[index]; + const char *qn = candidate->qualified_name; + size_t qn_len = qn ? strlen(qn) : 0; + if (!(candidate->flags & CBM_FUNC_FLAG_PROJECT_SOURCE) || qn_len <= name_len || + !strcmp(qn, target)) + continue; + const CTUModule *entry = + c_tu_for_declaration(ctx, candidate->source_file_qn, qn, qn_len - name_len - 1); + if ((entry && entry->at < at) || + c_is_own_declaration(ctx, candidate->source_file_qn, qn, qn_len - name_len - 1)) + return true; + } + size_t target_len = strlen(target); + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_PROTOTYPE) + continue; + const char *leaf = strrchr(decl->name, '.'); + if (strcmp(leaf ? leaf + 1 : decl->name, name)) + continue; + /* The same function, as `.` of its definition. */ + size_t relative_len = strlen(decl->name); + if (target_len > relative_len && target[target_len - relative_len - 1] == '.' && + !strcmp(target + target_len - relative_len, decl->name)) + continue; + const CTUModule *entry = c_tu_find(ctx, decl->file, strlen(decl->file)); + if (entry && !entry->exports_only && entry->at < at) + return true; + } + return false; +} static bool c_proof_call_domain(CLSPContext *ctx, TSNode call, TSNode function, const cbm_resolve_result_t *proof) { - if (!ctx->cpp_mode || proof->rule == CBM_RESOLVE_RULE_CALLABLE_BINDING || + if (!ctx->cpp_mode || ctx->plain_header || proof->rule == CBM_RESOLVE_RULE_CALLABLE_BINDING || strcmp(ts_node_type(function), "identifier")) return true; TSNode args = ts_node_child_by_field_name(call, "arguments", 9); uint32_t count = ts_node_named_child_count(args); + bool class_argument = false; for (uint32_t i = 0; i < count; i++) { TSNode argument = ts_node_named_child(args, i); const char *kind = ts_node_type(argument); if (c_proof_builtin_argument(ctx, argument)) continue; - if (strcmp(kind, "number_literal") && strcmp(kind, "char_literal") && - strcmp(kind, "string_literal") && strcmp(kind, "true") && strcmp(kind, "false") && - strcmp(kind, "null")) + if (!strcmp(kind, "comment") || !strcmp(kind, "number_literal") || + !strcmp(kind, "char_literal") || !strcmp(kind, "string_literal") || + !strcmp(kind, "concatenated_string") || !strcmp(kind, "true") || + !strcmp(kind, "false") || !strcmp(kind, "null")) + continue; + const cbm_resolve_proof_t *argument_proof = NULL; + const CBMType *shape = NULL; + if (!c_proof_expression_owner(ctx, argument, 0, &argument_proof, &shape)) return false; + class_argument = true; } - return true; + const char *name = c_node_text(ctx, function); + return !class_argument || + (name && proof->target && + !c_proof_other_visible_function(ctx, name, proof->target, ts_node_start_byte(call))); } // Resolve a name to a type (for identifiers used as types) @@ -4257,18 +4829,18 @@ static const CBMRegisteredType *c_field_owner_by_short_name(CLSPContext *ctx, co const char *qn = candidate->qualified_name; if (!qn || (members_only && !c_type_has_members(candidate))) continue; - for (int inc = 0; inc < ctx->include_count; inc++) { - const char *module = ctx->include_ns_qns[inc]; - size_t length = module ? strlen(module) : 0; - if (!length || strncmp(qn, module, length) || qn[length] != '.' || - strcmp(qn + length + 1, shortn)) - continue; - *found = true; - if (included && strcmp(included->qualified_name, qn)) - ambiguous = true; - included = candidate; - break; - } + size_t qn_len = strlen(qn), short_len = strlen(shortn); + if (qn_len <= short_len + 1 || qn[qn_len - short_len - 1] != '.' || + strcmp(qn + qn_len - short_len, shortn)) + continue; + const CTUModule *entry = + c_tu_for_declaration(ctx, candidate->source_file_qn, qn, qn_len - short_len - 1); + if (!entry || entry->exports_only) + continue; + *found = true; + if (included && strcmp(included->qualified_name, qn)) + ambiguous = true; + included = candidate; } return ambiguous ? NULL : included; } @@ -4343,35 +4915,101 @@ static const CBMRegisteredType *c_lookup_field_owner(CLSPContext *ctx, const cha * spelling. In particular, an unresolved `A` may have received the synthetic * current-module prefix; that prefix is not a declaration identity. */ static const CBMRegisteredType *c_proof_direct_class(CLSPContext *ctx, const char *qn, uint32_t at); +static const CBMRegisteredType *c_proof_field_class(CLSPContext *ctx, const char *qn, uint32_t at); +/* Declared inside a class, as `typedef detail::IEEEFloat IEEEFloat;` is in APFloat, a type's + * plain name means nothing outside that class. Code in a class with bases may inherit it. + * C has no such scope: a struct tag declared inside another struct is at file scope. */ +static bool c_nested_type_in_scope(const CLSPContext *ctx, const char *scope) { + if (!ctx->cpp_mode || ctx->plain_header) + return true; + const CBMRegisteredType *klass = ctx->proof_method_class; + if (!klass) + return ctx->enclosing_class_qn != NULL; + return !strcmp(klass->qualified_name, scope) || + (klass->embedded_types && klass->embedded_types[0]); +} + +/* Every declaration of this type name the translation unit sees. A name may be + * declared repeatedly: `typedef struct X X;` beside `struct X {...}`, or the same + * typedef in several headers. Those denote one type. Anything else is a conflict. */ static const CBMRegisteredType *c_proof_included_annotation(CLSPContext *ctx, const char *name, bool *found) { const CBMRegisteredType *chosen = NULL; *found = false; - for (int i = 0; i < ctx->include_count; i++) { - const char *module = ctx->include_ns_qns[i]; - const char *qn = module ? cbm_arena_sprintf(ctx->arena, "%s.%s", module, name) : NULL; - if (!qn) { - *found = true; - return NULL; - } - CBMTypeQNIter iterator; - cbm_registry_types_with_qn(ctx->proof_registry, qn, &iterator); - const CBMRegisteredType *candidate; - while ((candidate = cbm_type_qn_next(&iterator, NULL))) { - *found = true; - if (chosen && strcmp(chosen->qualified_name, candidate->qualified_name)) - return NULL; + size_t name_len = strlen(name); + CBMTypeShortIter it; + cbm_registry_types_by_short_name_chain(ctx->proof_registry, name, &it); + int index; + while ((index = cbm_type_short_iter_next(&it)) >= 0) { + const CBMRegisteredType *candidate = &it.reg->types[index]; + const char *qn = candidate->qualified_name; + size_t qn_len = qn ? strlen(qn) : 0; + if (qn_len <= name_len + 1 || qn[qn_len - name_len - 1] != '.' || + strcmp(qn + qn_len - name_len, name)) + continue; + size_t module_len = qn_len - name_len - 1; + const char *scope = cbm_arena_strndup(ctx->arena, qn, module_len); + if (scope && cbm_registry_lookup_type(ctx->proof_registry, scope) && + !c_nested_type_in_scope(ctx, scope)) + continue; + bool own = c_is_own_declaration(ctx, candidate->source_file_qn, qn, module_len); + const CTUModule *entry = + own ? NULL : c_tu_for_declaration(ctx, candidate->source_file_qn, qn, module_len); + if (!own && (!entry || entry->exports_only)) + continue; + *found = true; + if (!chosen || !strcmp(chosen->qualified_name, qn)) { chosen = candidate; + continue; } - if (!iterator.complete) { - *found = true; - return NULL; + if (chosen->alias_of && candidate->alias_of && + !strcmp(chosen->alias_of, candidate->alias_of)) + continue; + if (chosen->alias_of && !strcmp(chosen->alias_of, name)) { + chosen = candidate; + continue; } + if (candidate->alias_of && !strcmp(candidate->alias_of, name)) + continue; + return NULL; } return chosen; } +/* The declaration a type name denotes at this point: this file's own, or the + * single one its includes make visible. Explicit qualification stays intact. */ +static const CBMRegisteredType *c_proof_type_by_name(CLSPContext *ctx, const char *name) { + const CBMRegisteredType *local = c_resolve_field_owner_type(ctx, c_build_qn(ctx, name)); + /* Keep explicit qualification intact. An unqualified source name also + * searches every included module, retaining memberless and unsupported + * declarations as competitors. */ + if (strchr(name, '.') || strstr(name, "::")) + return local; + bool found = false; + const CBMRegisteredType *included = c_proof_included_annotation(ctx, name, &found); + if (found && (!included || (local && strcmp(local->qualified_name, included->qualified_name)))) + return NULL; + return included ? included : local; +} + +/* Through typedef names to what they alias, in the unit's own view of each name. + * A name that aliases itself (`typedef struct X X;`) means the tag of that name. */ +static const CBMRegisteredType *c_proof_follow_alias(CLSPContext *ctx, + const CBMRegisteredType *type) { + for (int hop = 0; type && type->alias_of && hop < 8; hop++) { + if (type->short_name && !strcmp(type->alias_of, type->short_name)) { + bool found = false; + const CBMRegisteredType *tag = c_proof_included_annotation(ctx, type->alias_of, &found); + if (!tag || tag == type || tag->alias_of) + return NULL; + return tag; + } + type = c_proof_type_by_name(ctx, type->alias_of); + } + return type && !type->alias_of ? type : NULL; +} + static const CBMRegisteredType *c_annotation_owner(CLSPContext *ctx, TSNode site) { TSNode annotation = ts_node_child_by_field_name(site, "type", 4); if (ts_node_is_null(annotation)) { @@ -4396,18 +5034,7 @@ static const CBMRegisteredType *c_annotation_owner(CLSPContext *ctx, TSNode site const char *name = c_node_text(ctx, annotation); if (!name) return NULL; - const CBMRegisteredType *local = c_resolve_field_owner_type(ctx, c_build_qn(ctx, name)); - /* Keep explicit qualification intact. An unqualified source name also - * searches exact include-module + name keys, retaining memberless and - * unsupported declarations as competitors. */ - if (strchr(name, '.') || strstr(name, "::")) - return local; - bool found = false; - const CBMRegisteredType *included = c_proof_included_annotation(ctx, name, &found); - if (found && - (!included || (local && strcmp(local->qualified_name, included->qualified_name)))) - return NULL; - return included ? included : local; + return c_proof_follow_alias(ctx, c_proof_type_by_name(ctx, name)); } return NULL; } @@ -4426,6 +5053,8 @@ static const CBMType *c_proof_annotation_type(CLSPContext *ctx, const CBMType *t c_proof_annotation_type(ctx, type->data.pointer.elem, owner, depth + 1); return inner ? cbm_type_pointer(ctx->arena, inner) : NULL; } + if (type->kind == CBM_TYPE_SLICE) + return c_proof_annotation_type(ctx, type->data.slice.elem, owner, depth + 1); if (type->kind == CBM_TYPE_REFERENCE || type->kind == CBM_TYPE_RVALUE_REF) { const CBMType *inner = c_proof_annotation_type(ctx, type->data.reference.elem, owner, depth + 1); @@ -4507,7 +5136,7 @@ static void c_observe_declared_binding(CLSPContext *ctx, const char *name, const const CBMRegisteredType *owner = c_annotation_owner(ctx, site); if (!owner || !owner->qualified_name) return; - owner = c_proof_direct_class(ctx, owner->qualified_name, ts_node_start_byte(site)); + owner = c_proof_field_class(ctx, owner->qualified_name, ts_node_start_byte(site)); if (!owner) return; cbm_resolve_result_t proof = cbm_lsp_proof_type_lookup( @@ -5546,6 +6175,34 @@ static void c_emit_resolved_call_at(CLSPContext *ctx, const char *callee_qn, con c_emit_resolved_call_orig_at(ctx, callee_qn, NULL, strategy, confidence, site); } +/* Is this node the object of an assignment or increment? `a->b = 1` writes b, and + * `a->b.c = 1` writes c while only reading b. */ +static bool c_written_lvalue(TSNode site) { + TSNode node = site; + TSNode parent = ts_node_parent(node); + /* The member half of `a->b` stands for the whole access. */ + if (!ts_node_is_null(parent) && !strcmp(ts_node_type(parent), "field_expression")) { + TSNode field = ts_node_child_by_field_name(parent, "field", 5); + if (ts_node_is_null(field) || !ts_node_eq(field, node)) + return false; + node = parent; + parent = ts_node_parent(node); + } + while (!ts_node_is_null(parent) && !strcmp(ts_node_type(parent), "parenthesized_expression")) { + node = parent; + parent = ts_node_parent(node); + } + if (ts_node_is_null(parent)) + return false; + const char *kind = ts_node_type(parent); + if (!strcmp(kind, "update_expression")) + return true; + if (strcmp(kind, "assignment_expression")) + return false; + TSNode left = ts_node_child_by_field_name(parent, "left", 4); + return !ts_node_is_null(left) && ts_node_eq(left, node); +} + static void c_emit_proven(CLSPContext *ctx, const cbm_resolve_result_t *proof, const char *source_name, CBMResolvedKind kind, TSNode site) { if (!ctx->resolved_calls || !ctx->enclosing_func_qn || ts_node_is_null(site) || @@ -5553,6 +6210,10 @@ static void c_emit_proven(CLSPContext *ctx, const cbm_resolve_result_t *proof, !cbm_resolve_admit(proof)) return; const char *relation = kind == CBM_RESOLVED_INVOCATION ? "CALLS" : "CALL_REFERENCE"; + if (kind == CBM_RESOLVED_TYPE_REFERENCE) + relation = "USAGE"; + if (kind == CBM_RESOLVED_VALUE_REFERENCE) + relation = c_written_lvalue(site) ? "WRITES" : "USAGE"; if (kind == CBM_RESOLVED_CALL_REFERENCE) { TSNode parent = ts_node_parent(site); const char *parent_kind = ts_node_is_null(parent) ? "" : ts_node_type(parent); @@ -5635,7 +6296,7 @@ static bool c_field_receiver_proven(CLSPContext *ctx, TSNode expression, const C const CBMRegisteredType *owner = c_annotation_owner(ctx, expression); if (!owner || !owner->qualified_name) return false; - owner = c_proof_direct_class(ctx, owner->qualified_name, ts_node_start_byte(expression)); + owner = c_proof_field_class(ctx, owner->qualified_name, ts_node_start_byte(expression)); if (!owner || cbm_type_is_unknown(observed)) return false; cbm_resolve_result_t proof = cbm_lsp_proof_type_lookup( @@ -5673,24 +6334,33 @@ static bool c_proof_type_visible(CLSPContext *ctx, const char *qn, uint32_t at) return false; if (c_proof_declared(ctx, key, at)) return true; - for (int i = 0; i < ctx->include_count; i++) { - const CProofInclude *include = &ctx->proof_includes[i]; - size_t length = strlen(include->module); - if (include->at < at && !strncmp(qn, include->module, length) && qn[length] == '.') - return true; - } - return false; + return c_tu_owner(ctx, qn, at) != NULL; } +static const CBMRegisteredType *c_proof_class_with(CLSPContext *ctx, const char *qn, uint32_t at, + uint32_t required); + +/* The whole class is understood: a member name not listed here does not exist. */ static const CBMRegisteredType *c_proof_direct_class(CLSPContext *ctx, const char *qn, uint32_t at) { + return c_proof_class_with(ctx, qn, at, + CBM_SOURCE_DECL_KNOWN | CBM_SOURCE_CLASS_DOMAIN_COMPLETE); +} + +/* The class is exact. A field written in its own body is that field whatever else + * the body holds: a directive, a nested type, a base class hidden by the member. */ +static const CBMRegisteredType *c_proof_field_class(CLSPContext *ctx, const char *qn, uint32_t at) { + return c_proof_class_with(ctx, qn, at, CBM_SOURCE_DECL_KNOWN); +} + +static const CBMRegisteredType *c_proof_class_with(CLSPContext *ctx, const char *qn, uint32_t at, + uint32_t required) { if (!qn || !c_proof_type_visible(ctx, qn, at)) return NULL; CBMTypeQNIter iterator; cbm_registry_types_with_qn(ctx->proof_registry, qn, &iterator); const CBMRegisteredType *chosen = NULL, *candidate; while ((candidate = cbm_type_qn_next(&iterator, NULL))) { - uint32_t required = CBM_SOURCE_DECL_KNOWN | CBM_SOURCE_CLASS_DOMAIN_COMPLETE; if (!candidate->project_source || !candidate->declaration_identity || !candidate->graph_qualified_name || (candidate->source_flags & required) != required || candidate->alias_of || candidate->type_param_names) @@ -5817,7 +6487,7 @@ static void c_publish_field_owner(CLSPContext *ctx, const char *type_qn, const c CBM_RESOLVE_FACT_RECEIVER_TYPED | CBM_RESOLVE_FACT_OCCURRENCE_EXACT}; if (receiver_proof) { const CBMRegisteredType *exact = - c_proof_direct_class(ctx, type_qn, ts_node_start_byte(site)); + c_proof_field_class(ctx, type_qn, ts_node_start_byte(site)); unsigned matches = 0, slot = 0; if (exact && exact == owner && exact->field_names) { unsigned i = 0; @@ -5858,11 +6528,12 @@ static void c_publish_field_owner(CLSPContext *ctx, const char *type_qn, const c detail, owner_proof); else proof.analysis = CBM_RESOLVE_ANALYSIS_FAILED; - /* This producer establishes a field value occurrence. Assignment - * and read/write classification need their own AST authority; a - * generic field record must not authorize every access relation. */ - proof = cbm_lsp_proof_occurrence(&ctx->field_types, proof, site, ctx->enclosing_func_qn, - field_name, "USAGE"); + /* This producer establishes a field value occurrence. Whether it is + * written is read here from the AST, the same way the extractor's + * assignment record does, and a write also serves as a usage. */ + proof = + cbm_lsp_proof_occurrence(&ctx->field_types, proof, site, ctx->enclosing_func_qn, + field_name, c_written_lvalue(site) ? "WRITES" : "USAGE"); resolved.evidence = cbm_lsp_proof_evidence(proof); } } @@ -5870,6 +6541,258 @@ static void c_publish_field_owner(CLSPContext *ctx, const char *type_qn, const c c_publish_resolved_call(ctx, resolved); } +static bool c_name_bound_locally(CLSPContext *ctx, const char *name); + +/* The base type name written in a field's declaration: `struct Ops *` is Ops. */ +static const char *c_field_type_name(const CBMType *type) { + for (int hop = 0; type && hop < 8; hop++) { + if (type->kind == CBM_TYPE_POINTER) { + type = type->data.pointer.elem; + } else if (type->kind == CBM_TYPE_SLICE) { + type = type->data.slice.elem; + } else if (type->kind == CBM_TYPE_REFERENCE || type->kind == CBM_TYPE_RVALUE_REF) { + type = type->data.reference.elem; + } else if (type->kind == CBM_TYPE_NAMED) { + const char *qn = type->data.named.qualified_name; + const char *dot = qn ? strrchr(qn, '.') : NULL; + return dot ? dot + 1 : qn; + } else { + return NULL; + } + } + return NULL; +} + +/* The written type of a file-scope object this unit sees. Every declaration of an + * object has one type, so a disagreement says this is not one object. */ +static const char *c_proof_value_type_name(CLSPContext *ctx, const char *name, uint32_t at) { + const char *type_name = NULL; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if ((decl->kind != CBM_C_DECL_EXTERN_VALUE && decl->kind != CBM_C_DECL_VALUE) || + strcmp(decl->name, name) || !decl->type_name) + continue; + if (strcmp(decl->file, c_own_file_key(ctx))) { + const CTUModule *entry = c_tu_find(ctx, decl->file, strlen(decl->file)); + if (!entry || entry->exports_only || entry->at >= at) + continue; + } + if (type_name && strcmp(type_name, decl->type_name)) + return NULL; + type_name = decl->type_name; + } + return type_name; +} + +static bool c_cxx_rules(const CLSPContext *ctx) { + return ctx->cpp_mode && !ctx->plain_header; +} + +static bool c_member_is_arrow(TSNode member) { + uint32_t count = ts_node_child_count(member); + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_child(member, i); + if (!ts_node_is_named(child) && !strcmp(ts_node_type(child), "->")) + return true; + } + return false; +} + +static const CBMType *c_shape_unwrap(const CBMType *shape) { + while (shape && (shape->kind == CBM_TYPE_REFERENCE || shape->kind == CBM_TYPE_RVALUE_REF)) + shape = shape->data.reference.elem; + return shape; +} + +/* In C++ `->` and `[]` may be overloaded operators, whose result is a different type. + * Only a written pointer or array makes them the builtin ones. */ +static bool c_cxx_access_is_builtin(const CLSPContext *ctx, TSNode member, const CBMType *shape) { + if (!c_cxx_rules(ctx)) + return true; + shape = c_shape_unwrap(shape); + bool pointer = shape && (shape->kind == CBM_TYPE_POINTER || shape->kind == CBM_TYPE_SLICE); + return c_member_is_arrow(member) ? pointer : (shape && !pointer); +} + +static const CBMRegisteredType *c_proof_named_class(CLSPContext *ctx, const char *type_name, + uint32_t at) { + const CBMRegisteredType *owner = + type_name ? c_proof_follow_alias(ctx, c_proof_type_by_name(ctx, type_name)) : NULL; + return owner ? c_proof_field_class(ctx, owner->qualified_name, at) : NULL; +} + +/* A base as the extractor wrote it, maybe `Base` or `ns::Base`: its plain name. */ +static const char *c_base_class_name(CLSPContext *ctx, const char *base) { + size_t length = strcspn(base, "<"); + const char *start = base; + for (size_t i = 0; i < length; i++) + if (base[i] == '.' || base[i] == ':') + start = base + i + 1; + return cbm_arena_strndup(ctx->arena, start, length - (size_t)(start - base)); +} + +/* The class that declares this field, the class itself first: a member hides what its + * bases declare, and a valid program cannot name one that two bases both declare. */ +static const CBMRegisteredType *c_proof_field_declarer(CLSPContext *ctx, + const CBMRegisteredType *klass, + const char *name, uint32_t at, int depth, + const CBMType **type) { + if (!klass || depth > 8) + return NULL; + for (unsigned i = 0; klass->field_names && i < 16384 && klass->field_names[i]; i++) { + if (strcmp(klass->field_names[i], name)) + continue; + if (type) + *type = klass->field_types ? klass->field_types[i] : NULL; + return klass; + } + for (int i = 0; klass->embedded_types && klass->embedded_types[i]; i++) { + const CBMRegisteredType *base = + c_proof_named_class(ctx, c_base_class_name(ctx, klass->embedded_types[i]), at); + const CBMRegisteredType *found = + c_proof_field_declarer(ctx, base, name, at, depth + 1, type); + if (found) + return found; + } + return NULL; +} + +/* Written proof that the member function's own class is this class. */ +static cbm_resolve_result_t c_proof_method_class_lookup(CLSPContext *ctx, TSNode site) { + const CBMRegisteredType *klass = ctx->proof_method_class; + cbm_resolve_result_t proof = cbm_lsp_proof_type_lookup( + &ctx->field_types, klass->short_name, site, "c-enclosing-class", klass->qualified_name, + klass->declaration_identity, klass->graph_qualified_name, klass->source_flags, + "member-function-of-this-class"); + return c_proof_type_import(ctx, klass->qualified_name, ts_node_start_byte(site), proof); +} + +/* The struct an expression denotes, as the written declarations along it establish + * it: a typed name, an extern object, a member of a known struct, or any of those + * under `*`, `&`, `[]` and parentheses. No initializer is consulted and nothing + * about the value flows in, so reassignment cannot make it stale. The shape is the + * written type of the expression so far, which C++ needs to read `->` and `[]`. */ +static const CBMRegisteredType *c_proof_expression_owner(CLSPContext *ctx, TSNode node, int depth, + const cbm_resolve_proof_t **proof, + const CBMType **shape) { + if (depth > 16 || ts_node_is_null(node) || ctx->eval_failure) + return NULL; + const char *kind = ts_node_type(node); + uint32_t at = ts_node_start_byte(node); + if (!strcmp(kind, "parenthesized_expression") && ts_node_named_child_count(node) == 1) + return c_proof_expression_owner(ctx, ts_node_named_child(node, 0), depth + 1, proof, shape); + if (!strcmp(kind, "subscript_expression") || !strcmp(kind, "pointer_expression")) { + const CBMRegisteredType *base = c_proof_expression_owner( + ctx, ts_node_child_by_field_name(node, "argument", 8), depth + 1, proof, shape); + if (!base) + return NULL; + TSNode op = ts_node_child_by_field_name(node, "operator", 8); + const char *op_text = ts_node_is_null(op) ? NULL : c_node_text(ctx, op); + if (op_text && !strcmp(op_text, "&")) { + *shape = *shape ? cbm_type_pointer(ctx->arena, *shape) : NULL; + return base; + } + const CBMType *written = c_shape_unwrap(*shape); + bool indexable = + written && (written->kind == CBM_TYPE_POINTER || written->kind == CBM_TYPE_SLICE); + if (c_cxx_rules(ctx) && !indexable) + return NULL; + *shape = !indexable ? NULL + : written->kind == CBM_TYPE_POINTER ? written->data.pointer.elem + : written->data.slice.elem; + return base; + } + if (!strcmp(kind, "field_expression")) { + const CBMRegisteredType *base = c_proof_expression_owner( + ctx, ts_node_child_by_field_name(node, "argument", 8), depth + 1, proof, shape); + TSNode field = ts_node_child_by_field_name(node, "field", 5); + const char *name = !base || ts_node_is_null(field) ? NULL : c_node_text(ctx, field); + if (!name || !c_cxx_access_is_builtin(ctx, node, *shape)) + return NULL; + const CBMType *written = NULL; + if (!c_proof_field_declarer(ctx, base, name, at, 0, &written)) + return NULL; + *shape = written; + return c_proof_named_class(ctx, c_field_type_name(written), at); + } + if (!strcmp(kind, "call_expression")) { + /* What a call returns is written on the function it selects. */ + ctx->proof_class_scope_checked = false; + cbm_resolve_result_t callee = + c_callable_proof(ctx, ts_node_child_by_field_name(node, "function", 8)); + const CBMRegisteredFunc *function = + cbm_resolve_admit(&callee) + ? cbm_registry_lookup_func(ctx->proof_registry, callee.target) + : NULL; + const CBMType *written = function && function->signature && + function->signature->kind == CBM_TYPE_FUNC && + function->signature->data.func.return_types + ? function->signature->data.func.return_types[0] + : NULL; + const CBMRegisteredType *owner = c_proof_named_class(ctx, c_field_type_name(written), at); + if (!owner) + return NULL; + *proof = callee.proof; + *shape = written; + return owner; + } + if (!strcmp(kind, "this")) { + if (!ctx->proof_method_class) + return NULL; + cbm_resolve_result_t typed = c_proof_method_class_lookup(ctx, node); + if (!cbm_resolve_admit(&typed)) + return NULL; + *proof = typed.proof; + *shape = cbm_type_pointer( + ctx->arena, cbm_type_named(ctx->arena, ctx->proof_method_class->qualified_name)); + return ctx->proof_method_class; + } + if (strcmp(kind, "identifier")) + return NULL; + const char *name = c_node_text(ctx, node); + if (!name) + return NULL; + cbm_resolve_type_fact_t fact = + cbm_resolve_types_lookup(&ctx->field_types, ctx->current_scope, name, at); + if (fact.status == CBM_RESOLVE_TYPE_KNOWN && fact.evidence.target && fact.evidence.proof) { + *proof = fact.evidence.proof; + *shape = fact.type; + return c_proof_field_class(ctx, fact.evidence.target, at); + } + /* A name bound in this function that the fact above does not type is opaque. */ + if (c_name_bound_locally(ctx, name)) + return NULL; + /* `Name.size()` inside a member function reads this class's own field. */ + if (ctx->proof_method_class) { + const CBMType *written = NULL; + if (!c_proof_field_declarer(ctx, ctx->proof_method_class, name, at, 0, &written)) + return NULL; + const CBMRegisteredType *owner = c_proof_named_class(ctx, c_field_type_name(written), at); + cbm_resolve_result_t typed = + owner ? c_proof_method_class_lookup(ctx, node) : (cbm_resolve_result_t){0}; + if (!owner || !cbm_resolve_admit(&typed)) + return NULL; + *proof = typed.proof; + *shape = written; + return owner; + } + const char *type_name = c_proof_value_type_name(ctx, name, at); + const CBMRegisteredType *owner = c_proof_named_class(ctx, type_name, at); + if (!owner) + return NULL; + cbm_resolve_result_t typed = cbm_lsp_proof_type_lookup( + &ctx->field_types, name, node, "c-extern-object-type", owner->qualified_name, + owner->declaration_identity, owner->graph_qualified_name, owner->source_flags, + "visible-extern-declaration;written-type"); + typed = c_proof_type_import(ctx, owner->qualified_name, at, typed); + if (!cbm_resolve_admit(&typed)) + return NULL; + *proof = typed.proof; + *shape = NULL; + return owner; +} + static void c_emit_field_reference(CLSPContext *ctx, TSNode node) { if (!ctx->resolved_calls || !ctx->enclosing_func_qn) return; @@ -5881,10 +6804,22 @@ static void c_emit_field_reference(CLSPContext *ctx, TSNode node) { char *field_name = c_node_text(ctx, field); if (!field_name || !field_name[0]) return; + const cbm_resolve_proof_t *owner_proof = NULL; + const CBMType *owner_shape = NULL; + const CBMRegisteredType *owner = + c_proof_expression_owner(ctx, argument, 0, &owner_proof, &owner_shape); + if (owner && owner_proof && c_cxx_access_is_builtin(ctx, node, owner_shape)) { + const CBMRegisteredType *declarer = + c_proof_field_declarer(ctx, owner, field_name, ts_node_start_byte(node), 0, NULL); + if (declarer) { + c_publish_field_owner(ctx, declarer->qualified_name, field_name, field, owner_proof); + return; + } + } const CBMType *object_type = c_eval_expr_type(ctx, argument); const char *type_qn = NULL; const cbm_resolve_proof_t *receiver_proof = NULL; - if (cbm_type_is_unknown(object_type) || + if (cbm_type_is_unknown(object_type) || !c_cxx_access_is_builtin(ctx, node, object_type) || !c_field_receiver_proven(ctx, argument, object_type, 0, &type_qn, &receiver_proof)) return; bool is_arrow = false; @@ -6025,6 +6960,506 @@ static void c_walk_initializer_list(CLSPContext *ctx, TSNode list) { } } +/* A binding nearer than file scope: a parameter or a block-scope name. */ +static bool c_name_bound_locally(CLSPContext *ctx, const char *name) { + for (const CBMScope *scope = ctx->current_scope; scope && scope->parent; scope = scope->parent) + if (cbm_scope_lookup_local(scope, name)) + return true; + return false; +} + +/* The name a declarator introduces, through `*`, `[]` and `(*name)(int)`. */ +static TSNode c_declarator_identifier(TSNode declarator) { + for (int hop = 0; hop < 16 && !ts_node_is_null(declarator); hop++) { + const char *kind = ts_node_type(declarator); + if (!strcmp(kind, "identifier")) + return declarator; + TSNode inner = ts_node_child_by_field_name(declarator, "declarator", 10); + if (ts_node_is_null(inner) && !strcmp(kind, "parenthesized_declarator") && + ts_node_named_child_count(declarator)) + inner = ts_node_named_child(declarator, 0); + declarator = inner; + } + return (TSNode){0}; +} + +/* A local name hides a file-scope one from its declaration on, even when nothing + * else about it is understood: a callback parameter `int (*fn)(int)`, for one. */ +static void c_bind_declared_names(CLSPContext *ctx, TSNode node, const char *kind) { + if (strcmp(kind, "parameter_declaration") && strcmp(kind, "optional_parameter_declaration") && + strcmp(kind, "declaration")) + return; + TSTreeCursor cursor = ts_tree_cursor_new(node); + if (ts_tree_cursor_goto_first_child(&cursor)) + do { + const char *field = ts_tree_cursor_current_field_name(&cursor); + if (!field || strcmp(field, "declarator")) + continue; + TSNode name = c_declarator_identifier(ts_tree_cursor_current_node(&cursor)); + const char *text = ts_node_is_null(name) ? NULL : c_node_text(ctx, name); + if (text && !cbm_scope_lookup_local(ctx->current_scope, text)) + c_scope_bind(ctx, text, cbm_type_unknown()); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + ts_tree_cursor_delete(&cursor); +} + +/* An extern declaration of this object the translation unit sees. */ +static const CBMCDecl *c_proof_extern_value(CLSPContext *ctx, const char *name, uint32_t at, + const CTUModule **via) { + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_EXTERN_VALUE || strcmp(decl->name, name)) + continue; + if (!strcmp(decl->file, c_own_file_key(ctx))) + return decl; + const CTUModule *entry = c_tu_find(ctx, decl->file, strlen(decl->file)); + if (entry && !entry->exports_only && entry->at < at) { + *via = entry; + return decl; + } + } + return NULL; +} + +/* An exact graph declaration chosen by the rules around this call. */ +static cbm_resolve_result_t c_proof_declaration_lookup(CLSPContext *ctx, const char *spelling, + TSNode site, const char *target, + const char *decision, bool imported) { + bool located = !ts_node_is_null(site); + cbm_resolve_proof_key_t key = {.lookup_domain = spelling, + .lookup_key = spelling, + .declaration_identity = target, + .start_byte = located ? ts_node_start_byte(site) : 0, + .end_byte = located ? ts_node_end_byte(site) : 0, + .binding_identity = target, + .detail = decision}; + cbm_resolve_result_t audit = {.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, .reason = decision}; + const cbm_resolve_proof_t *inputs[2]; + inputs[0] = cbm_resolve_proof_leaf(&ctx->field_types.proofs, CBM_PROOF_ADAPTER_DOMAIN, + &ctx->field_types.proof_source, &key, &audit); + audit.binding = CBM_RESOLVE_BINDING_UNIQUE; + audit.domain = CBM_RESOLVE_DOMAIN_PROJECT; + audit.target = target; + audit.facts = CBM_RESOLVE_FACT_EXACT_TARGET; + audit.candidate_count = 1; + key.detail = "selected_declaration"; + inputs[1] = cbm_resolve_proof_leaf(&ctx->field_types.proofs, CBM_PROOF_REGISTERED_DECLARATION, + NULL, &key, &audit); + cbm_resolve_result_t result = { + .binding = CBM_RESOLVE_BINDING_UNIQUE, + .domain = CBM_RESOLVE_DOMAIN_PROJECT, + .analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, + .rule = imported ? CBM_RESOLVE_RULE_IMPORT_BINDING : CBM_RESOLVE_RULE_SCOPE_BINDING, + .facts = CBM_RESOLVE_FACT_EXACT_TARGET | CBM_RESOLVE_FACT_LOOKUP_COMPLETE | + CBM_RESOLVE_FACT_OCCURRENCE_EXACT | + (imported ? CBM_RESOLVE_FACT_IMPORT_BOUND : CBM_RESOLVE_FACT_SCOPE_BOUND), + .target = target, + .candidate_count = 1, + .reason = decision}; + result.proof = + cbm_resolve_proof_derive(&ctx->field_types.proofs, CBM_PROOF_REGISTRY_LOOKUP, + &ctx->field_types.proof_source, &key, &result, inputs, 2, true); + if (!result.proof || result.proof->value.outcome.analysis != CBM_RESOLVE_ANALYSIS_COMPLETE) + return c_proof_missing("c_declaration_lookup_failed"); + return result; +} + +/* As above, for a member selected through a receiver whose type is proven. */ +static cbm_resolve_result_t c_proof_member_lookup(CLSPContext *ctx, const char *spelling, + TSNode site, const char *target, + const cbm_resolve_proof_t *receiver) { + cbm_resolve_proof_key_t key = {.lookup_domain = spelling, + .lookup_key = spelling, + .declaration_identity = target, + .start_byte = ts_node_start_byte(site), + .end_byte = ts_node_end_byte(site), + .binding_identity = target, + .detail = "member-of-the-receiver-class"}; + cbm_resolve_result_t audit = {.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, + .reason = "member-of-the-receiver-class"}; + const cbm_resolve_proof_t *inputs[3]; + inputs[0] = cbm_resolve_proof_leaf(&ctx->field_types.proofs, CBM_PROOF_ADAPTER_DOMAIN, + &ctx->field_types.proof_source, &key, &audit); + audit.binding = CBM_RESOLVE_BINDING_UNIQUE; + audit.domain = CBM_RESOLVE_DOMAIN_PROJECT; + audit.target = target; + audit.facts = CBM_RESOLVE_FACT_EXACT_TARGET; + audit.candidate_count = 1; + key.detail = "selected_member_declaration"; + inputs[1] = cbm_resolve_proof_leaf(&ctx->field_types.proofs, CBM_PROOF_REGISTERED_DECLARATION, + NULL, &key, &audit); + inputs[2] = receiver; + cbm_resolve_result_t result = { + .binding = CBM_RESOLVE_BINDING_UNIQUE, + .domain = CBM_RESOLVE_DOMAIN_PROJECT, + .analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, + .rule = CBM_RESOLVE_RULE_TYPE_MEMBER, + .facts = CBM_RESOLVE_FACT_EXACT_TARGET | CBM_RESOLVE_FACT_LOOKUP_COMPLETE | + CBM_RESOLVE_FACT_RECEIVER_TYPED | CBM_RESOLVE_FACT_OCCURRENCE_EXACT, + .target = target, + .candidate_count = 1, + .reason = "member-of-the-receiver-class"}; + result.proof = + cbm_resolve_proof_derive(&ctx->field_types.proofs, CBM_PROOF_REGISTRY_LOOKUP, + &ctx->field_types.proof_source, &key, &result, inputs, 3, true); + if (!result.proof || result.proof->value.outcome.analysis != CBM_RESOLVE_ANALYSIS_COMPLETE) + return c_proof_missing("c_member_lookup_failed"); + return result; +} + +typedef struct { + const char *target; /* the one member found so far */ + bool conflict; /* several members fit and nothing tells them apart */ + bool open; /* a base class is not understood, so more may exist */ +} CMemberHit; + +/* One member, spelled with its signature on one side at most. */ +static bool c_same_member_target(const char *a, const char *b) { + bool a_signature = strchr(a, '(') != NULL, b_signature = strchr(b, '(') != NULL; + if (a_signature == b_signature) + return !strcmp(a, b); + size_t length = strlen(a_signature ? b : a); + return !strncmp(a, b, length); +} + +static void c_member_hit_add(CMemberHit *hit, const char *target) { + if (hit->target && !c_same_member_target(hit->target, target)) + hit->conflict = true; + if (!hit->target || !strchr(hit->target, '(')) + hit->target = target; +} + +/* The member function this name selects in this class. A class's own declaration hides + * its bases. Written out of line it has no node under the class: its definition is the + * one whose own file includes the class. Overloads are told apart by parameter count, + * which a default argument makes a maximum, or not at all. */ +static void c_proof_class_method(CLSPContext *ctx, const CBMRegisteredType *klass, const char *name, + int argc, uint32_t at, int depth, bool from_own_class, + CMemberHit *hit) { + if (!klass || depth > 8) + return; + bool same_class = ctx->proof_method_class && ctx->proof_method_class == klass; + /* A member function reaches its bases' protected members, and no further. */ + bool from_derived = !same_class && from_own_class; + /* What the class body says about this name, which a definition elsewhere does not: + * that it is declared at all, and with what access. */ + bool declared = false; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_MEMBER_NAME || strcmp(decl->name, name) || !decl->type_name || + !klass->short_name || strcmp(decl->type_name, klass->short_name) || + !klass->source_file_qn || strcmp(decl->file, klass->source_file_qn)) + continue; + declared = true; + if (!same_class && (decl->flags & CBM_C_DECL_NOT_PUBLIC) && + !(from_derived && (decl->flags & CBM_C_DECL_PROTECTED))) { + hit->open = true; + return; + } + } + CMemberHit own = {0}; + CBMMethodIter iterator; + cbm_registry_methods_chain(ctx->proof_registry, klass->qualified_name, name, &iterator); + for (int index; (index = cbm_method_iter_next(&iterator)) >= 0;) { + const CBMRegisteredFunc *candidate = &iterator.reg->funcs[index]; + if (!(candidate->flags & CBM_FUNC_FLAG_PROJECT_SOURCE) || candidate->type_param_names || + !(candidate->source_flags & CBM_SOURCE_DECL_KNOWN) || + (!same_class && (candidate->source_flags & CBM_SOURCE_DECL_PRIVATE)) || + (!same_class && !from_derived && + (candidate->source_flags & CBM_SOURCE_DECL_PROTECTED)) || + (argc >= 0 && candidate->source_arity && candidate->source_arity != argc + 1)) + continue; + const char *target = + candidate->sig && candidate->sig[0] + ? cbm_arena_sprintf(ctx->arena, "%s%s", candidate->qualified_name, candidate->sig) + : candidate->qualified_name; + if (target) + c_member_hit_add(&own, target); + } + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_METHOD || strcmp(decl->name, name) || !decl->target || + !decl->type_name || !klass->short_name || strcmp(decl->type_name, klass->short_name) || + !klass->source_file_qn || (decl->arity >= 0 && argc > decl->arity)) + continue; + if (c_proof_file_reaches(ctx, decl->file, klass->source_file_qn)) + c_member_hit_add(&own, decl->target); + } + if (own.target) { + if (own.conflict) + hit->conflict = true; + c_member_hit_add(hit, own.target); + return; + } + /* The class declares this name and nothing here can be bound to it. It hides the + * bases and the enclosing scopes all the same. */ + if (declared) { + hit->open = true; + return; + } + for (int i = 0; klass->embedded_types && klass->embedded_types[i]; i++) { + const CBMRegisteredType *base = + c_proof_named_class(ctx, c_base_class_name(ctx, klass->embedded_types[i]), at); + if (!base) + hit->open = true; + c_proof_class_method(ctx, base, name, argc, at, depth + 1, + from_own_class || ctx->proof_method_class == klass, hit); + } + /* A class whose body is not fully understood may hold more. */ + if (!(klass->source_flags & CBM_SOURCE_CLASS_BODY_COMPLETE)) + hit->open = true; +} + +static int c_call_argument_count(TSNode call) { + TSNode arguments = ts_node_child_by_field_name(call, "arguments", 9); + uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments); + int total = 0; + for (uint32_t i = 0; i < count; i++) + total += strcmp(ts_node_type(ts_node_named_child(arguments, i)), "comment") != 0; + return total; +} + +/* `Type::member(...)`: the scope names a class the translation unit sees, and the member + * is selected in it by the same rules as through a receiver. */ +static cbm_resolve_result_t c_proof_scoped_member(CLSPContext *ctx, TSNode function, TSNode call) { + TSNode scope = ts_node_child_by_field_name(function, "scope", 5); + TSNode name = ts_node_child_by_field_name(function, "name", 4); + if (ts_node_is_null(scope) || ts_node_is_null(name) || + strcmp(ts_node_type(scope), "namespace_identifier") || + strcmp(ts_node_type(name), "identifier")) + return c_proof_missing("c_scope_is_not_a_plain_name"); + const char *owner = c_node_text(ctx, scope); + const char *written = c_node_text(ctx, name); + uint32_t at = ts_node_start_byte(call); + const CBMRegisteredType *klass = c_proof_named_class(ctx, owner, at); + if (!klass || !written) + return c_proof_missing("c_scope_class_not_visible"); + CMemberHit hit = {0}; + c_proof_class_method(ctx, klass, written, c_call_argument_count(call), at, 0, false, &hit); + if (!hit.target || hit.conflict) + return c_proof_missing("c_scoped_member_not_selected"); + cbm_resolve_result_t typed = cbm_lsp_proof_type_lookup( + &ctx->field_types, owner, scope, "c-written-class-scope", klass->qualified_name, + klass->declaration_identity, klass->graph_qualified_name, klass->source_flags, + "class-named-by-the-call's-scope"); + typed = c_proof_type_import(ctx, klass->qualified_name, at, typed); + return cbm_resolve_admit(&typed) ? c_proof_member_lookup(ctx, c_node_text(ctx, function), + function, hit.target, typed.proof) + : typed; +} + +/* A file-scope object or enumerator in the translation unit. An extern declaration + * means the one definition of that external name in the project, as for functions. */ +static cbm_resolve_result_t c_proof_value(CLSPContext *ctx, const char *name, TSNode site) { + uint32_t at = ts_node_start_byte(site); + cbm_resolve_result_t result = {.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, + .reason = "c_visible_value_missing"}; + const char *chosen = NULL; + const CTUModule *chosen_via = NULL; + const char *external = NULL; + int external_count = 0; + for (const CBMCDeclLink *link = cbm_c_decl_lookup(ctx->c_decls, name); link; + link = link->next) { + const CBMCDecl *decl = link->decl; + if (decl->kind != CBM_C_DECL_VALUE || strcmp(decl->name, name) || !decl->target) + continue; + if ((decl->flags & CBM_C_DECL_EXTERNAL) && strcmp(external ? external : "", decl->target)) { + external = decl->target; + external_count++; + } + const CTUModule *entry = NULL; + if (strcmp(decl->file, c_own_file_key(ctx))) { + entry = c_tu_find(ctx, decl->file, strlen(decl->file)); + if (!entry || entry->exports_only || entry->at >= at) + continue; + } + if (chosen && strcmp(chosen, decl->target)) + return (cbm_resolve_result_t){.binding = CBM_RESOLVE_BINDING_AMBIGUOUS, + .analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, + .candidate_count = 2, + .reason = "c_visible_values_conflict"}; + chosen = decl->target; + chosen_via = entry; + } + if (chosen) { + result = c_proof_declaration_lookup(ctx, name, site, chosen, "visible-file-scope-value", + chosen_via != NULL); + return chosen_via + ? c_proof_import_join(ctx, result, chosen_via->via, CBM_PROOF_IMPORT_BINDING) + : result; + } + const CTUModule *via = NULL; + const CBMCDecl *declared = c_proof_extern_value(ctx, name, at, &via); + if (!declared) + return result; + if (external_count > 1) + return (cbm_resolve_result_t){.analysis = CBM_RESOLVE_ANALYSIS_COMPLETE, + .binding = CBM_RESOLVE_BINDING_AMBIGUOUS, + .candidate_count = 2, + .reason = "c_extern_value_definition_not_unique"}; + /* With no definition in the project, the declaration itself is what the name + * selects, when it has a node of its own. */ + if (!external && declared->target) + external = declared->target; + if (!external) + return result; + result = c_proof_declaration_lookup( + ctx, name, site, external, "visible-extern-declaration;unique-external-definition", false); + return via ? c_proof_import_join(ctx, result, via->via, CBM_PROOF_IMPORT_BINDING) : result; +} + +/* An argument of a function-like macro is tokens, not an expression: neovim's + * `HAS_KEY(opts, win_text_height, start_row)` pastes both names into another identifier. So a + * bare name there says nothing about a declaration that happens to have that name. */ +static bool c_bare_argument_of_macro(CLSPContext *ctx, TSNode node) { + TSNode list = ts_node_parent(node); + if (ts_node_is_null(list) || strcmp(ts_node_type(list), "argument_list")) + return false; + TSNode call = ts_node_parent(list); + if (ts_node_is_null(call) || strcmp(ts_node_type(call), "call_expression")) + return false; + TSNode function = ts_node_child_by_field_name(call, "function", 8); + if (ts_node_is_null(function) || strcmp(ts_node_type(function), "identifier")) + return false; + const char *name = c_node_text(ctx, function); + return name && c_proof_macro_visible(ctx, name, ts_node_start_byte(call)); +} + +static void c_emit_value_reference(CLSPContext *ctx, TSNode node) { + if (!ctx->c_decls || !ctx->enclosing_func_qn || ctx->in_template || + c_bare_argument_of_macro(ctx, node)) + return; + TSNode parent = ts_node_parent(node); + const char *parent_kind = ts_node_is_null(parent) ? "" : ts_node_type(parent); + /* Only expression positions: a declarator names what it declares, and a callee + * is the call's own proof. */ + static const char *const not_values[] = { + "init_declarator", "pointer_declarator", "array_declarator", "function_declarator", + "parameter_declaration", "declaration", "attribute_specifier", "preproc_defined", + "qualified_identifier", "field_designator", "enumerator"}; + for (size_t i = 0; i < sizeof(not_values) / sizeof(not_values[0]); i++) { + if (strcmp(parent_kind, not_values[i])) + continue; + TSNode value = ts_node_child_by_field_name(parent, "value", 5); + if (ts_node_is_null(value) || !ts_node_eq(value, node)) + return; + } + if (!strcmp(parent_kind, "call_expression")) { + TSNode function = ts_node_child_by_field_name(parent, "function", 8); + if (!ts_node_is_null(function) && ts_node_eq(function, node)) + return; + } + const char *name = c_node_text(ctx, node); + if (!name || c_name_bound_locally(ctx, name) || + c_proof_macro_visible(ctx, name, ts_node_start_byte(node))) + return; + if (ctx->proof_method_class) { + /* Inside a member function the class comes first. A field read here has no + * receiver in the source: the class of the function is the receiver. */ + uint32_t at = ts_node_start_byte(node); + const CBMRegisteredType *declarer = + c_proof_field_declarer(ctx, ctx->proof_method_class, name, at, 0, NULL); + if (declarer) { + cbm_resolve_result_t typed = c_proof_method_class_lookup(ctx, node); + const char *field = + cbm_arena_sprintf(ctx->arena, "%s.%s", declarer->qualified_name, name); + if (field && cbm_resolve_admit(&typed)) { + cbm_resolve_result_t member = + c_proof_member_lookup(ctx, name, node, field, typed.proof); + if (cbm_resolve_admit(&member)) + c_emit_proven(ctx, &member, name, CBM_RESOLVED_VALUE_REFERENCE, node); + } + return; + } + CMemberHit hit = {0}; + c_proof_class_method(ctx, ctx->proof_method_class, name, -1, at, 0, false, &hit); + if (hit.target || hit.open) + return; + } + cbm_resolve_result_t proof = c_proof_value(ctx, name, node); + if (cbm_resolve_admit(&proof)) + c_emit_proven(ctx, &proof, name, CBM_RESOLVED_VALUE_REFERENCE, node); +} + +/* A function can declare its own `enum { LIMIT = 8 }` or `struct node { ... }`. Those + * names are in block scope and hide any file-scope namesake from here on. */ +static void c_bind_local_tag_and_enumerators(CLSPContext *ctx, TSNode node, const char *kind) { + if (strcmp(kind, "struct_specifier") && strcmp(kind, "union_specifier") && + strcmp(kind, "enum_specifier")) + return; + TSNode body = ts_node_child_by_field_name(node, "body", 4); + if (ts_node_is_null(body)) + return; + TSNode tag = ts_node_child_by_field_name(node, "name", 4); + const char *tag_name = ts_node_is_null(tag) ? NULL : c_node_text(ctx, tag); + if (tag_name) + c_scope_bind(ctx, tag_name, cbm_type_unknown()); + if (strcmp(kind, "enum_specifier")) + return; + uint32_t count = ts_node_named_child_count(body); + for (uint32_t i = 0; i < count; i++) { + TSNode item = ts_node_named_child(body, i); + TSNode name = strcmp(ts_node_type(item), "enumerator") + ? (TSNode){0} + : ts_node_child_by_field_name(item, "name", 4); + const char *text = ts_node_is_null(name) ? NULL : c_node_text(ctx, name); + if (text) + c_scope_bind(ctx, text, cbm_type_unknown()); + } +} + +/* A written type name. Its declaration in this translation unit is the target, + * which is a typedef's own node, not what the typedef aliases. */ +static void c_emit_type_reference(CLSPContext *ctx, TSNode node) { + if (!ctx->enclosing_func_qn || ctx->in_template) + return; + /* `ns::Name` and `Tpl::x` are not looked up from here. */ + TSNode parent = ts_node_parent(node); + const char *parent_kind = ts_node_is_null(parent) ? "" : ts_node_type(parent); + if (!strcmp(parent_kind, "qualified_identifier") || !strcmp(parent_kind, "template_type") || + !strcmp(parent_kind, "scoped_type_identifier")) + return; + /* `struct node {...}` defines a tag here rather than using one. */ + if ((!strcmp(parent_kind, "struct_specifier") || !strcmp(parent_kind, "union_specifier") || + !strcmp(parent_kind, "enum_specifier")) && + !ts_node_is_null(ts_node_child_by_field_name(parent, "body", 4))) + return; + const char *name = c_node_text(ctx, node); + if (!name || !ctx->module_qn || c_name_bound_locally(ctx, name) || + c_proof_macro_visible(ctx, name, ts_node_start_byte(node))) + return; + const CBMRegisteredType *type = c_proof_type_by_name(ctx, name); + uint32_t at = ts_node_start_byte(node); + if (!type || !type->project_source || !type->graph_qualified_name || + !c_proof_type_visible(ctx, type->qualified_name, at)) + return; + /* A tag and an alias of the same spelling are one name to a C reader. Several + * different typedefs of it are not, whichever the lookup above happened to keep. */ + cbm_resolve_result_t proof = cbm_lsp_proof_type_lookup( + &ctx->field_types, name, node, "c-written-type-name", type->qualified_name, + type->declaration_identity, type->graph_qualified_name, type->source_flags, + "visible-type-declaration"); + proof = c_proof_type_import(ctx, type->qualified_name, at, proof); + if (cbm_resolve_admit(&proof)) + c_emit_proven(ctx, &proof, name, CBM_RESOLVED_TYPE_REFERENCE, node); +} + +/* The types in a function's own signature belong to that function's usage. */ +static void c_emit_signature_types(CLSPContext *ctx, TSNode node, int depth) { + if (ts_node_is_null(node) || depth > 32) + return; + if (!strcmp(ts_node_type(node), "type_identifier")) { + c_emit_type_reference(ctx, node); + return; + } + uint32_t count = ts_node_named_child_count(node); + for (uint32_t i = 0; i < count; i++) + c_emit_signature_types(ctx, ts_node_named_child(node, i), depth + 1); +} + /* 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 @@ -6037,8 +7472,9 @@ static void c_resolve_callable_argument_references(CLSPContext *ctx, TSNode call for (uint32_t i = 0; i < count; i++) { TSNode argument = ts_node_named_child(arguments, i); const char *kind = ts_node_type(argument); - if (!strcmp(kind, "identifier") || !strcmp(kind, "qualified_identifier") || - !strcmp(kind, "scoped_identifier")) { + if ((!strcmp(kind, "identifier") || !strcmp(kind, "qualified_identifier") || + !strcmp(kind, "scoped_identifier")) && + !c_bare_argument_of_macro(ctx, argument)) { cbm_resolve_result_t proof = c_callable_proof(ctx, argument); c_emit_proven(ctx, &proof, c_node_text(ctx, argument), CBM_RESOLVED_CALL_REFERENCE, argument); @@ -6109,67 +7545,20 @@ static void c_resolve_calls_in_node(CLSPContext *ctx, TSNode node) { ctx->walk_depth--; } -static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { - if (ts_node_is_null(node)) - return; - const char *kind = ts_node_type(node); - - if (ctx->test_definition_owners && ctx->test_definition_owners->has_test_definition_owners && - ctx->test_definition_owners->test_owner_language == CBM_LANG_C) { - if (c_process_configured_test_body(ctx, node)) - return; - /* Nested native functions are independent owners even when they occur - * inside the raw body of a configured C definition. */ - if (strcmp(kind, "function_definition") == 0) { - c_process_function(ctx, node); - return; - } +/* What this one node does, apart from walking its children. Kept out of the recursive + * function: its locals would otherwise be in every frame of the walk, and + * C_LSP_MAX_WALK_DEPTH nested frames have to fit the stack. */ +__attribute__((noinline)) static void c_resolve_node_operations(CLSPContext *ctx, TSNode node, + const char *kind) { + if (!strcmp(kind, "identifier")) + c_emit_value_reference(ctx, node); + else if (!strcmp(kind, "type_identifier")) + c_emit_type_reference(ctx, node); + else if (ctx->current_scope && ctx->current_scope->parent) { + c_bind_local_tag_and_enumerators(ctx, node, kind); + c_bind_declared_names(ctx, node, kind); } - /* Declarations establish scope before their initializer. Assignments - * take effect after their operands, never before a nested RHS write. */ - bool assignment = !strcmp(kind, "assignment_expression"); - if (!assignment) - c_process_statement(ctx, node); - int initializer_fp = -1; - const cbm_resolve_proof_t *initializer_origin = NULL; - if (!strcmp(kind, "init_declarator")) { - TSNode name = - c_fp_declarator_name(ctx, ts_node_child_by_field_name(node, "declarator", 10), NULL); - if (!ts_node_is_null(name)) - initializer_fp = c_find_fp_binding_index(ctx, c_node_text(ctx, name)); - if (initializer_fp >= 0) - initializer_origin = ctx->fp_target_proofs[initializer_fp]; - } - - // --- Initializers: hand the initialized type down to the list --- - if (strcmp(kind, "initializer_list") == 0) { - c_walk_initializer_list(ctx, node); - return; - } - if (strcmp(kind, "init_declarator") == 0) { - c_hint_declared_initializer(ctx, node); - } else if (strcmp(kind, "compound_literal_expression") == 0) { - TSNode literal_type = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(literal_type)) - c_hint_initializer_list(ctx, ts_node_child_by_field_name(node, "value", 5), - c_parse_type_node(ctx, literal_type)); - } - - /* Taking a callable variable's address exposes its storage to writes - * that this local assignment tracker cannot see. */ - if (!strcmp(kind, "pointer_expression") || !strcmp(kind, "unary_expression")) { - TSNode operand = - c_fp_unwrap_parentheses(ctx, ts_node_child_by_field_name(node, "argument", 8)); - TSNode operator= ts_node_child_by_field_name(node, "operator", 8); - const char *op = ts_node_is_null(operator) ? NULL : c_node_text(ctx, operator); - if (op && !strcmp(op, "&") && !ts_node_is_null(operand) && - !strcmp(ts_node_type(operand), "identifier")) - c_escape_fp_storage(ctx, c_node_text(ctx, operand)); - } - - c_resolve_callable_value_reference(ctx, node); - // --- Member access: publish which type owns the field --- if (strcmp(kind, "field_expression") == 0) { c_emit_field_reference(ctx, node); @@ -6181,7 +7570,34 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { TSNode func_node = ts_node_child_by_field_name(node, "function", 8); if (!ts_node_is_null(func_node)) { const char *fk = ts_node_type(func_node); + /* Inside a member function the class's own scope comes first, and an + * unqualified name it might declare through an unknown base is unknown. */ + ctx->proof_class_scope_checked = false; + if (ctx->proof_method_class && !ctx->in_template && !strcmp(fk, "identifier")) { + const char *written = c_node_text(ctx, func_node); + uint32_t at = ts_node_start_byte(node); + if (written && !c_name_bound_locally(ctx, written) && + !c_proof_macro_visible(ctx, written, at)) { + CMemberHit hit = {0}; + c_proof_class_method(ctx, ctx->proof_method_class, written, + c_call_argument_count(node), at, 0, false, &hit); + if (hit.target && !hit.conflict) { + cbm_resolve_result_t typed = c_proof_method_class_lookup(ctx, node); + cbm_resolve_result_t member = + cbm_resolve_admit(&typed) + ? c_proof_member_lookup(ctx, written, func_node, hit.target, + typed.proof) + : typed; + c_emit_proven(ctx, &member, written, CBM_RESOLVED_INVOCATION, node); + } + if (hit.target || hit.open || + c_proof_field_declarer(ctx, ctx->proof_method_class, written, at, 0, NULL)) + return; + ctx->proof_class_scope_checked = true; + } + } cbm_resolve_result_t proof = c_callable_proof(ctx, func_node); + ctx->proof_class_scope_checked = false; if (cbm_resolve_admit(&proof) && c_proof_call_domain(ctx, node, func_node, &proof)) { proof = cbm_lsp_proof_rule( &ctx->field_types, proof, node, "c-call-lookup-domain", @@ -6189,7 +7605,16 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { "qualified-or-bound-callable-or-no-associated-argument-types", NULL); c_emit_proven(ctx, &proof, c_node_text(ctx, func_node), CBM_RESOLVED_INVOCATION, node); - goto recurse; + return; + } + + if (c_cxx_rules(ctx) && !ctx->in_template && !strcmp(fk, "qualified_identifier")) { + cbm_resolve_result_t scoped = c_proof_scoped_member(ctx, func_node, node); + if (cbm_resolve_admit(&scoped)) { + c_emit_proven(ctx, &scoped, c_node_text(ctx, func_node), + CBM_RESOLVED_INVOCATION, node); + return; + } } // field_expression: obj.method() or ptr->method() @@ -6198,7 +7623,33 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { if (cbm_resolve_admit(&method)) { c_emit_proven(ctx, &method, c_node_text(ctx, func_node), CBM_RESOLVED_INVOCATION, node); - goto recurse; + return; + } + if (c_cxx_rules(ctx) && !ctx->in_template) { + const cbm_resolve_proof_t *receiver_proof = NULL; + const CBMType *receiver_shape = NULL; + const CBMRegisteredType *receiver = c_proof_expression_owner( + ctx, ts_node_child_by_field_name(func_node, "argument", 8), 0, + &receiver_proof, &receiver_shape); + TSNode member_name = ts_node_child_by_field_name(func_node, "field", 5); + const char *written = + ts_node_is_null(member_name) ? NULL : c_node_text(ctx, member_name); + uint32_t at = ts_node_start_byte(node); + /* A field of this name holds a callable, not a member function. */ + if (receiver && receiver_proof && written && + c_cxx_access_is_builtin(ctx, func_node, receiver_shape) && + !c_proof_field_declarer(ctx, receiver, written, at, 0, NULL)) { + CMemberHit hit = {0}; + c_proof_class_method(ctx, receiver, written, c_call_argument_count(node), + at, 0, false, &hit); + if (hit.target && !hit.conflict) { + const char *spelling = c_node_text(ctx, func_node); + cbm_resolve_result_t member = c_proof_member_lookup( + ctx, spelling, func_node, hit.target, receiver_proof); + c_emit_proven(ctx, &member, spelling, CBM_RESOLVED_INVOCATION, node); + return; + } + } } TSNode arg_node = ts_node_child_by_field_name(func_node, "argument", 8); TSNode field_node = ts_node_child_by_field_name(func_node, "field", 5); @@ -6269,7 +7720,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { strategy = "lsp_smart_ptr_dispatch"; c_emit_resolved_call_at(ctx, method->qualified_name, strategy, 0.95f, node); - goto recurse; + return; } } @@ -6297,7 +7748,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { } } } - goto recurse; + return; } // qualified_identifier: ns::func() or Class::static_method() @@ -6348,7 +7799,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { f = cbm_registry_lookup_func(ctx->registry, qn); if (f) { c_emit_resolved_call_at(ctx, f->qualified_name, "lsp_scoped", 0.95f, node); - goto recurse; + return; } // Try as method (Class::method) with type-aware overload resolution const char *dot = strrchr(qn, '.'); @@ -6369,7 +7820,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { if (m) { c_emit_resolved_call_at(ctx, m->qualified_name, "lsp_scoped", 0.95f, node); - goto recurse; + return; } } // Namespace fallback: extract_defs may omit namespace from QN. @@ -6386,7 +7837,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { if (nf) { c_emit_resolved_call_at(ctx, nf->qualified_name, "lsp_scoped", 0.90f, node); - goto recurse; + return; } } // Class::method of a class declared in another file (#1153). @@ -6404,12 +7855,12 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { if (sm) { c_emit_resolved_call_at(ctx, sm->qualified_name, "lsp_scoped", 0.95f, node); - goto recurse; + return; } } c_emit_unresolved_call(ctx, qn, "scoped_not_in_registry"); } - goto recurse; + return; } // template_function: func(args) or ns::func(args) @@ -6469,7 +7920,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { } } } - goto recurse; + return; } // Direct identifier call: func() @@ -6489,7 +7940,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { c_emit_resolved_call_orig_at(ctx, fp_target, name, is_dll ? "lsp_dll_resolve" : "lsp_func_ptr", is_dll ? 0.80f : 0.85f, node); - goto recurse; + return; } // Check if it's a variable with callable type @@ -6497,7 +7948,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { if (!cbm_type_is_unknown(var_type)) { if (var_type->kind == CBM_TYPE_FUNC) { // Function type variable without tracked target — can't resolve - goto recurse; + return; } // Functor call: variable with operator() const CBMType *base = c_simplify_type(ctx, var_type, false); @@ -6508,7 +7959,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { if (op) { c_emit_resolved_call_at(ctx, op->qualified_name, "lsp_operator", 0.90f, node); - goto recurse; + return; } } } @@ -6524,7 +7975,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { const char *ctor_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn_str, name); c_emit_resolved_call_at(ctx, ctor_qn, "lsp_constructor", 0.95f, node); - goto recurse; + return; } } @@ -6562,7 +8013,7 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { } } } - goto recurse; + return; } } } @@ -6954,8 +8405,70 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { // being initialized This is primarily for completeness — no CALLS edge // needed for field designators } +} + +static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node) { + if (ts_node_is_null(node)) + return; + const char *kind = ts_node_type(node); + + if (ctx->test_definition_owners && ctx->test_definition_owners->has_test_definition_owners && + ctx->test_definition_owners->test_owner_language == CBM_LANG_C) { + if (c_process_configured_test_body(ctx, node)) + return; + /* Nested native functions are independent owners even when they occur + * inside the raw body of a configured C definition. */ + if (strcmp(kind, "function_definition") == 0) { + c_process_function(ctx, node); + return; + } + } + + /* Declarations establish scope before their initializer. Assignments + * take effect after their operands, never before a nested RHS write. */ + bool assignment = !strcmp(kind, "assignment_expression"); + if (!assignment) + c_process_statement(ctx, node); + int initializer_fp = -1; + const cbm_resolve_proof_t *initializer_origin = NULL; + if (!strcmp(kind, "init_declarator")) { + TSNode name = + c_fp_declarator_name(ctx, ts_node_child_by_field_name(node, "declarator", 10), NULL); + if (!ts_node_is_null(name)) + initializer_fp = c_find_fp_binding_index(ctx, c_node_text(ctx, name)); + if (initializer_fp >= 0) + initializer_origin = ctx->fp_target_proofs[initializer_fp]; + } + + // --- Initializers: hand the initialized type down to the list --- + if (strcmp(kind, "initializer_list") == 0) { + c_walk_initializer_list(ctx, node); + return; + } + if (strcmp(kind, "init_declarator") == 0) { + c_hint_declared_initializer(ctx, node); + } else if (strcmp(kind, "compound_literal_expression") == 0) { + TSNode literal_type = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(literal_type)) + c_hint_initializer_list(ctx, ts_node_child_by_field_name(node, "value", 5), + c_parse_type_node(ctx, literal_type)); + } + + /* Taking a callable variable's address exposes its storage to writes + * that this local assignment tracker cannot see. */ + if (!strcmp(kind, "pointer_expression") || !strcmp(kind, "unary_expression")) { + TSNode operand = + c_fp_unwrap_parentheses(ctx, ts_node_child_by_field_name(node, "argument", 8)); + TSNode operator= ts_node_child_by_field_name(node, "operator", 8); + const char *op = ts_node_is_null(operator) ? NULL : c_node_text(ctx, operator); + if (op && !strcmp(op, "&") && !ts_node_is_null(operand) && + !strcmp(ts_node_type(operand), "identifier")) + c_escape_fp_storage(ctx, c_node_text(ctx, operand)); + } + + c_resolve_callable_value_reference(ctx, node); + c_resolve_node_operations(ctx, node, kind); -recurse:; // Push scope for blocks and control structures bool push_scope = (strcmp(kind, "compound_statement") == 0 || strcmp(kind, "if_statement") == 0 || @@ -7154,6 +8667,41 @@ static char *c_macro_wrapped_func_name(CLSPContext *ctx, char *func_name, TSNode return cbm_arena_sprintf(ctx->arena, "%.*s::%s", (int)(last_sep - func_name), func_name, arg); } +/* The class a member function belongs to: where its body is written, or the class + * named in `Name::f` for one written elsewhere. `ns::f` names no class. */ +static const CBMRegisteredType *c_proof_function_class(CLSPContext *ctx, TSNode func_node) { + if (!c_cxx_rules(ctx) || ctx->in_template) + return NULL; + uint32_t at = ts_node_start_byte(func_node); + /* For `Name::f` this QN is a guess at this module's own class. A class declared in + * a header under a namespace is found by the name written in the declarator. */ + const CBMRegisteredType *enclosing = + ctx->enclosing_class_qn ? c_proof_field_class(ctx, ctx->enclosing_class_qn, at) : NULL; + if (enclosing) + return enclosing; + TSNode declarator = ts_node_child_by_field_name(func_node, "declarator", 10); + for (int hop = 0; hop < 16 && !ts_node_is_null(declarator); hop++) { + const char *kind = ts_node_type(declarator); + if (!strcmp(kind, "qualified_identifier")) { + TSNode scope = ts_node_child_by_field_name(declarator, "scope", 5); + TSNode name = ts_node_child_by_field_name(declarator, "name", 4); + const char *inner = ts_node_is_null(name) ? "" : ts_node_type(name); + if (!strcmp(inner, "qualified_identifier")) { + declarator = name; + continue; + } + const char *text = ts_node_is_null(scope) ? NULL : c_node_text(ctx, scope); + return text ? c_proof_named_class(ctx, c_base_class_name(ctx, text), at) : NULL; + } + TSNode next = ts_node_child_by_field_name(declarator, "declarator", 10); + if (ts_node_is_null(next) && !strcmp(kind, "reference_declarator") && + ts_node_named_child_count(declarator)) + next = ts_node_named_child(declarator, ts_node_named_child_count(declarator) - 1); + declarator = next; + } + return NULL; +} + static void c_process_function(CLSPContext *ctx, TSNode func_node) { if (ctx->eval_failure) return; @@ -7163,6 +8711,12 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { if (ts_node_is_null(decl)) return; + /* `void f(int a) ATTR { ... }` under the C++ grammar reads as a function named + * ATTR with no parameters, its real name and parameters lost in an ERROR. Which + * function this body belongs to, and what its parameters hide, is not known. */ + if (!strcmp(ts_node_type(decl), "identifier")) + return; + // Extract function name from declarator chain char *func_name = NULL; const char *saved_class_qn = ctx->enclosing_class_qn; @@ -7306,6 +8860,13 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { * (gap #5a). */ const char *dot = strrchr(func_qn, '.'); func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_class_qn, dot + 1); + } else if (strchr(func_qn, '.')) { + /* `Name::f` whose class is declared in another file, or `ns::f`: the class + * lookup above found nothing in this module, and the def walk writes this + * definition directly under the file's module, whatever namespace block it + * sits in. */ + if (ctx->module_qn) + func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, func_qn); } else if (!strchr(func_qn, '.')) { /* A free function in a namespace is qualified by the namespace scope * (current_namespace is module_qn.ns), matching the def QN the extractor @@ -7318,6 +8879,8 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { } } ctx->enclosing_func_qn = func_qn; + const CBMRegisteredType *saved_method_class = ctx->proof_method_class; + ctx->proof_method_class = c_proof_function_class(ctx, func_node); // If inside a template, attach type_param_names to the registered function // so pending template calls can be resolved at call sites. NEVER do this @@ -7344,6 +8907,9 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { // Push function scope CBMScope *saved_scope = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); + uint32_t saved_syntax_limit = ctx->proof_syntax_limit; + if (!saved_func_qn || saved_func_qn == ctx->module_qn) + c_proof_enter_function(ctx, func_node); // Bind 'this' if in a method if (ctx->enclosing_class_qn) { @@ -7368,6 +8934,7 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { defaulted_params++; } c_process_statement(ctx, param); + c_bind_declared_names(ctx, param, pk); } } } @@ -7380,6 +8947,9 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { } } + c_emit_signature_types(ctx, ts_node_child_by_field_name(func_node, "type", 4), 0); + c_emit_signature_types(ctx, params_node, 0); + // Walk function body TSNode body = ts_node_child_by_field_name(func_node, "body", 4); if (!ts_node_is_null(body)) { @@ -7390,6 +8960,8 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { ctx->current_scope = saved_scope; ctx->enclosing_class_qn = saved_class_qn; ctx->enclosing_func_qn = saved_func_qn; + ctx->proof_syntax_limit = saved_syntax_limit; + ctx->proof_method_class = saved_method_class; } // ============================================================================ @@ -7402,7 +8974,10 @@ static bool c_is_body_container(const char *kind) { return strcmp(kind, "preproc_if") == 0 || strcmp(kind, "preproc_ifdef") == 0 || strcmp(kind, "preproc_else") == 0 || strcmp(kind, "preproc_elif") == 0 || strcmp(kind, "preproc_elifdef") == 0 || strcmp(kind, "linkage_specification") == 0 || - strcmp(kind, "declaration_list") == 0; + strcmp(kind, "declaration_list") == 0 || + /* An #if that splits a brace leaves the rest of the file inside one ERROR, + * and complete functions sit in it with their own scopes intact. */ + strcmp(kind, "ERROR") == 0; } // Process a top-level or nested declaration within a namespace/class body, @@ -7970,6 +9545,20 @@ static const CBMType *c_parse_return_type_text(CBMArena *a, const char *text, if (!text || !text[0]) return cbm_type_unknown(); + /* An aggregate defined where its member is declared, `union vval_union { ... } vval;`, + * has its whole body in the type text. The tag before it is the type. */ + const char *body_open = strchr(text, '{'); + const char *body_close = body_open ? strrchr(body_open, '}') : NULL; + if (body_close) { + size_t prefix = (size_t)(body_open - text); + while (prefix > 0 && text[prefix - 1] == ' ') + prefix--; + const char *joined = cbm_arena_sprintf(a, "%.*s%s", (int)prefix, text, body_close + 1); + if (!joined) + return cbm_type_unknown(); + text = joined; + } + // Skip const/volatile qualifiers and the elaborated-type keyword: a member // declared `struct Ops *ops;` has the type text `struct Ops`, and the // struct is registered as `Ops`. @@ -8380,7 +9969,7 @@ bool cbm_run_c_lsp_with_test_owners(CBMArena *arena, CBMFileResult *result, cons ts_parser_delete(parser); return false; } - owner_tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)source_len); + owner_tree = cbm_parse_c_source(parser, source, (uint32_t)source_len, (TSParseOptions){0}); ts_parser_delete(parser); if (!owner_tree) return false; @@ -8619,8 +10208,12 @@ static void c_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const cha rt.project_source = true; rt.graph_qualified_name = d->graph_qualified_name; rt.declaration_identity = d->declaration_identity; + rt.source_file_qn = d->source_file_qn; rt.source_flags = d->source_flags; rt.is_interface = d->is_interface; + /* `typedef struct Tag Name;`, `typedef Other Name;`: the written name. + * Each use resolves it in its own translation unit. */ + rt.alias_of = d->alias_target; // Embedded/base types if (d->embedded_types) { @@ -8702,6 +10295,8 @@ static void c_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const cha rf.graph_qualified_name = d->graph_qualified_name; rf.source_flags = d->source_flags; rf.source_signature = d->source_signature; + rf.source_arity = d->source_arity; + rf.source_file_qn = d->source_file_qn; rf.source_namespace = d->namespace_name; if (rf.source_flags & CBM_SOURCE_DECL_FIXED_ARITY) rf.min_params = d->signature_param_count; @@ -8773,6 +10368,7 @@ CBMTypeRegistry *cbm_c_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, in } cbm_registry_finalize(reg); cbm_registry_build_type_short_index(reg); + reg->c_decls = cbm_c_decl_index_build(arena, defs, def_count); reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */ return reg; } @@ -8808,7 +10404,7 @@ static bool c_lsp_cross_with_registry_run( return false; const TSLanguage *ts_lang = cpp_mode ? tree_sitter_cpp() : tree_sitter_c(); ts_parser_set_language(parser, ts_lang); - tree = cbm_parse_source(parser, source, (uint32_t)source_len, (TSParseOptions){0}); + tree = cbm_parse_c_source(parser, source, (uint32_t)source_len, (TSParseOptions){0}); ts_parser_delete(parser); owns_tree = true; if (!tree) @@ -8935,7 +10531,7 @@ static bool c_lsp_cross_run(CBMArena *arena, const char *source, int source_len, return false; const TSLanguage *ts_lang = cpp_mode ? tree_sitter_cpp() : tree_sitter_c(); ts_parser_set_language(parser, ts_lang); - tree = cbm_parse_source(parser, source, (uint32_t)source_len, (TSParseOptions){0}); + tree = cbm_parse_c_source(parser, source, (uint32_t)source_len, (TSParseOptions){0}); ts_parser_delete(parser); owns_tree = true; if (!tree) @@ -8948,6 +10544,7 @@ static bool c_lsp_cross_run(CBMArena *arena, const char *source, int source_len, // comment for the rationale (linear-scan fallback otherwise). cbm_registry_finalize(®); cbm_registry_build_type_short_index(®); + reg.c_decls = cbm_c_decl_index_build(arena, defs, def_count); // Initialize context and run CLSPContext ctx; @@ -9082,7 +10679,7 @@ bool cbm_batch_c_lsp_cross(CBMArena *arena, CBMBatchCLSPFile *files, int file_co cbm_arena_init(&file_arena); cbm_resolve_proof_pool_t copied_proofs; - cbm_resolve_proof_pool_init(&copied_proofs, arena, 0, 0, 0); + cbm_resolve_proof_pool_init_project(&copied_proofs, arena); CBMResolvedCallArray file_out; memset(&file_out, 0, sizeof(file_out)); diff --git a/internal/cbm/lsp/c_lsp.h b/internal/cbm/lsp/c_lsp.h index e12629ce1..46e80f6d8 100644 --- a/internal/cbm/lsp/c_lsp.h +++ b/internal/cbm/lsp/c_lsp.h @@ -24,6 +24,17 @@ typedef struct { struct CProofDeclaration *proof_declarations; int proof_declaration_count, proof_declaration_cap; struct CProofInclude *proof_includes; + /* The translation unit: every file this one includes, through other files + * too, with the point in this file where it became visible. */ + struct CTUModule *tu_modules; + uint32_t tu_cap, tu_count; + const struct CBMCDeclIndex *c_decls; + const char *own_file_qn; /* NULL when only the module identifies this file */ + /* C++: the class whose member function this is, inline or out of line. Its members + * are found before anything at namespace scope. */ + const CBMRegisteredType *proof_method_class; + bool proof_class_scope_checked; /* the current name was looked for in that class */ + bool plain_header; /* this TU is a .h, so its own language is its includer's */ const cbm_resolve_import_snapshot_t *import_facts; bool require_import_proofs; struct CProofBuiltinBinding *proof_builtins; @@ -173,7 +184,7 @@ struct CLSPEvalMemoSlot { /* Conservative syntax audit shared with the source-fact collector. Only direct, * fixed-arity free signatures with explicitly written builtin types qualify. */ -bool cbm_c_preprocessor_domain_unsupported(TSNode root); +bool cbm_c_preprocessor_domain_unsupported(TSNode root, const char *source); const CBMType *cbm_c_builtin_annotation(CBMArena *arena, const char *source, TSNode type); const CBMType *cbm_c_free_signature(CBMArena *arena, const char *source, TSNode declaration, bool cpp_mode); diff --git a/internal/cbm/lsp/c_proof.c b/internal/cbm/lsp/c_proof.c index 9e9188037..ee201cfa0 100644 --- a/internal/cbm/lsp/c_proof.c +++ b/internal/cbm/lsp/c_proof.c @@ -17,7 +17,14 @@ typedef struct { bool failed; bool extern_c; bool preprocessor_unknown; + bool conditional; + /* C has one linkage for every name. A plain .h is parsed by the C++ grammar + * like everything here, but its language is its includer's. */ + bool c_language; + /* `#ifdef __cplusplus extern "C" { #endif`: the brace the grammar cannot pair. */ + bool file_extern_c; const char *module; + CBMCFileFacts *facts; } csp_context_t; static bool csp_is(TSNode node, const char *kind) { @@ -55,13 +62,20 @@ static void csp_mark(csp_context_t *ctx, TSNode node, const char *qn, uint32_t f entry->definition->source_flags = flags; if (arity >= 0 && entry->definition->signature_param_count == arity) entry->definition->source_flags |= CBM_SOURCE_DECL_FIXED_ARITY; + entry->definition->source_arity = arity >= 0 ? arity + 1 : 0; } static TSNode csp_function_declarator(TSNode node) { TSNode decl = csp_field(node, "declarator"); for (unsigned depth = 0; depth < 32 && !ts_node_is_null(decl); depth++) { if (csp_is(decl, "function_declarator")) return decl; - if (!csp_is(decl, "pointer_declarator") && !csp_is(decl, "reference_declarator")) + if (csp_is(decl, "reference_declarator")) { + /* `T &f()`: the grammar gives this declarator's inner part no field name. */ + uint32_t count = ts_node_named_child_count(decl); + decl = count ? ts_node_named_child(decl, count - 1) : (TSNode){0}; + continue; + } + if (!csp_is(decl, "pointer_declarator")) break; decl = csp_field(decl, "declarator"); } @@ -92,18 +106,39 @@ static int csp_arity(csp_context_t *ctx, TSNode declarator) { ts_tree_cursor_delete(&cursor); return count; } -/* Raw directives can rename either a callable or a builtin type. The adapter - * does not borrow authority from a different preprocessor configuration. */ -bool cbm_c_preprocessor_domain_unsupported(TSNode root) { +/* A signature compares builtin spellings, so only a macro that respells a + * builtin type word can falsify one. Conditionals and ordinary macros change + * which declarations exist, which the declaration facts record per name. */ +static bool csp_builtin_word(const char *text, uint32_t length) { + static const char *const words[] = {"void", "bool", "_Bool", "char", "short", + "int", "long", "float", "double", "signed", + "unsigned", "wchar_t", "char8_t", "char16_t", "char32_t"}; + for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) + if (strlen(words[i]) == length && !memcmp(text, words[i], length)) + return true; + return false; +} + +bool cbm_c_preprocessor_domain_unsupported(TSNode root, const char *source) { TSTreeCursor cursor = ts_tree_cursor_new(root); uint32_t visited = 0; bool unsupported = false; for (;;) { TSNode node = ts_tree_cursor_current_node(&cursor); - if (++visited > 1000000 || !strncmp(ts_node_type(node), "preproc_", 8)) { + const char *kind = ts_node_type(node); + if (++visited > 1000000) { unsupported = true; break; } + if (!strcmp(kind, "preproc_def") || !strcmp(kind, "preproc_function_def")) { + TSNode name = ts_node_child_by_field_name(node, "name", 4); + if (!ts_node_is_null(name) && + csp_builtin_word(source + ts_node_start_byte(name), + ts_node_end_byte(name) - ts_node_start_byte(name))) { + unsupported = true; + break; + } + } if (ts_tree_cursor_goto_first_child(&cursor)) continue; while (!ts_tree_cursor_goto_next_sibling(&cursor)) { @@ -236,28 +271,193 @@ const CBMType *cbm_c_free_signature(CBMArena *arena, const char *source, TSNode return cbm_type_func(arena, NULL, types, returns); } +static CBMCDecl *csp_add_decl(csp_context_t *ctx, CBMCDeclKind kind, const char *name, + uint32_t flags) { + CBMCFileFacts *facts = ctx->facts; + if (!facts || !name) + return NULL; + if (facts->decl_count == facts->decl_cap) { + int cap = facts->decl_cap ? facts->decl_cap * 2 : 16; + CBMCDecl *next = cbm_arena_alloc(ctx->arena, (size_t)cap * sizeof(*next)); + if (!next) { + ctx->failed = true; + return NULL; + } + if (facts->decl_count) + memcpy(next, facts->decls, (size_t)facts->decl_count * sizeof(*next)); + facts->decls = next; + facts->decl_cap = cap; + } + CBMCDecl *decl = &facts->decls[facts->decl_count++]; + *decl = (CBMCDecl){.file = facts->file_qn ? facts->file_qn : ctx->module, + .name = name, + .arity = -1, + .kind = kind, + .flags = flags | (ctx->conditional ? CBM_C_DECL_CONDITIONAL : 0)}; + return decl; +} + +static bool csp_storage_is(csp_context_t *ctx, TSNode node, const char *storage) { + for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { + TSNode child = ts_node_named_child(node, i); + const char *text = csp_is(child, "storage_class_specifier") ? csp_text(ctx, child) : NULL; + if (text && !strcmp(text, storage)) + return true; + } + return false; +} + +/* Name relative to the file: a namespace is part of a C++ linkage key, the file + * module never is. */ +static const char *csp_relative(csp_context_t *ctx, const char *owner, const char *name) { + size_t module_len = strlen(ctx->module); + if (!strcmp(owner, ctx->module)) + return name; + if (!strncmp(owner, ctx->module, module_len) && owner[module_len] == '.') + return cbm_arena_sprintf(ctx->arena, "%s.%s", owner + module_len + 1, name); + return NULL; +} + +/* A parameter list ending in `...` is not a fixed count, and `()` in C is not a + * prototype at all: it says nothing of the parameters. */ +static int csp_fixed_arity(csp_context_t *ctx, TSNode decl) { + TSNode params = csp_field(decl, "parameters"); + if (ts_node_is_null(params) || !ts_node_named_child_count(params)) + return -1; + uint32_t count = ts_node_child_count(params); + for (uint32_t i = 0; i < count; i++) + if (csp_is(ts_node_child(params, i), "variadic_parameter") || + csp_is(ts_node_child(params, i), "...")) + return -1; + return csp_arity(ctx, decl); +} + +static void csp_note_prototype(csp_context_t *ctx, TSNode node, TSNode decl, const char *owner, + const char *name) { + bool is_static = csp_storage_is(ctx, node, "static"); + uint32_t flags = (is_static ? 0 : CBM_C_DECL_EXTERNAL) | + (ctx->extern_c || ctx->file_extern_c ? CBM_C_DECL_C_LINKAGE : 0); + CBMCDecl *fact = csp_add_decl(ctx, CBM_C_DECL_PROTOTYPE, csp_relative(ctx, owner, name), flags); + if (!fact) + return; + fact->arity = csp_fixed_arity(ctx, decl); + fact->signature = ctx->preprocessor_unknown + ? NULL + : cbm_c_free_signature(ctx->arena, ctx->source, node, !ctx->c_language); +} + +/* `struct Tag`, `Tag *`, `const Tag`: the name a member access needs. */ +static const char *csp_base_type_name(csp_context_t *ctx, TSNode type) { + if (csp_is(type, "struct_specifier") || csp_is(type, "union_specifier") || + csp_is(type, "class_specifier")) + type = csp_field(type, "name"); + return csp_is(type, "type_identifier") ? csp_text(ctx, type) : NULL; +} + +/* Enumerators sit in file scope whichever declaration wrote them, with no linkage. */ +static void csp_note_enumerators(csp_context_t *ctx, TSNode type, const char *owner) { + if (!ctx->facts || !csp_is(type, "enum_specifier") || strcmp(owner, ctx->module)) + return; + TSNode list = csp_field(type, "body"); + uint32_t count = ts_node_is_null(list) ? 0 : ts_node_named_child_count(list); + for (uint32_t i = 0; i < count; i++) { + TSNode item = ts_node_named_child(list, i); + const char *name = + csp_is(item, "enumerator") ? csp_text(ctx, csp_field(item, "name")) : NULL; + CBMCDecl *fact = name ? csp_add_decl(ctx, CBM_C_DECL_VALUE, name, 0) : NULL; + if (fact) + fact->target = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module, name); + } +} + +/* Objects, not functions: `extern struct redisServer server;` has no graph node + * of its own, and a file-scope `static` or enumerator has no external linkage. */ +static void csp_note_values(csp_context_t *ctx, TSNode node, const char *owner) { + if (!ctx->facts || strcmp(owner, ctx->module)) + return; + csp_note_enumerators(ctx, csp_field(node, "type"), owner); + bool is_extern = csp_storage_is(ctx, node, "extern"); + bool is_static = csp_storage_is(ctx, node, "static"); + const char *type_name = csp_base_type_name(ctx, csp_field(node, "type")); + /* `EXTERN buf_T *curbuf;` reads as type EXTERN with buf_T stranded in an ERROR: + * a storage macro in front of the real type. */ + for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { + TSNode child = ts_node_named_child(node, i); + if (csp_is(child, "ERROR") && ts_node_named_child_count(child) == 1 && + csp_is(ts_node_named_child(child, 0), "identifier")) + type_name = csp_text(ctx, ts_node_named_child(child, 0)); + } + TSTreeCursor cursor = ts_tree_cursor_new(node); + if (ts_tree_cursor_goto_first_child(&cursor)) + do { + const char *field = ts_tree_cursor_current_field_name(&cursor); + if (!field || strcmp(field, "declarator")) + continue; + TSNode declarator = ts_tree_cursor_current_node(&cursor); + bool initialized = csp_is(declarator, "init_declarator"); + for (unsigned depth = 0; depth < 16 && !ts_node_is_null(declarator); depth++) { + if (csp_is(declarator, "function_declarator")) + break; + if (csp_is(declarator, "identifier")) { + const char *name = csp_text(ctx, declarator); + CBMCDecl *fact = + is_extern && !initialized + ? csp_add_decl(ctx, CBM_C_DECL_EXTERN_VALUE, name, CBM_C_DECL_EXTERNAL) + : csp_add_decl(ctx, CBM_C_DECL_VALUE, name, + is_static ? 0 : CBM_C_DECL_EXTERNAL); + if (fact) { + fact->type_name = type_name; + fact->target = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module, name); + } + break; + } + declarator = csp_field(declarator, "declarator"); + } + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + ts_tree_cursor_delete(&cursor); +} + +static void csp_note_macro(csp_context_t *ctx, TSNode node) { + TSNode name = csp_field(node, "name"); + if (!csp_is(name, "identifier")) + return; + const char *text = csp_text(ctx, name); + if (text && !(ctx->facts && ctx->facts->guard && !strcmp(ctx->facts->guard, text))) + (void)csp_add_decl(ctx, CBM_C_DECL_MACRO, text, 0); +} + static void csp_free_function(csp_context_t *ctx, TSNode node, const char *owner) { TSNode decl = csp_function_declarator(node), name_node = csp_field(decl, "declarator"); - if (!csp_is(name_node, "identifier")) + /* Lua declares its whole API as `LUA_API int (lua_gettop) (lua_State *L);`. */ + while (csp_is(name_node, "parenthesized_declarator")) + name_node = ts_node_named_child(name_node, 0); + if (!csp_is(name_node, "identifier")) { + if (csp_is(node, "declaration")) + csp_note_values(ctx, node, owner); return; + } const char *name = csp_text(ctx, name_node); const char *qn = name ? cbm_arena_sprintf(ctx->arena, "%s.%s", owner, name) : NULL; + if (csp_is(node, "declaration") && name && !ts_node_has_error(decl)) + csp_note_prototype(ctx, node, decl, owner, name); csp_entry_t *entry = csp_entry(ctx, qn, ts_node_start_point(node).row + 1); - if (!entry || entry->duplicate) + /* A macro over a builtin type word (`#define int double`) falsifies what the + * signature below would say, so the definition's linkage facts stay unknown. */ + if (!entry || entry->duplicate || ctx->preprocessor_unknown) return; CBMLSPDef *def = entry->definition; bool cpp = def->lang == CBM_LANG_CPP || def->lang == CBM_LANG_CUDA; def->source_flags = CBM_SOURCE_DECL_KNOWN | CBM_SOURCE_DECL_TOP_LEVEL; + if (ctx->conditional) + def->source_flags |= CBM_SOURCE_DECL_CONDITIONAL; if (csp_is(node, "function_definition")) def->source_flags |= CBM_SOURCE_FUNCTION_DEFINITION; - if (!cpp || ctx->extern_c) + if (!cpp || ctx->extern_c || ctx->file_extern_c) def->source_flags |= CBM_SOURCE_DECL_EXTERN_C; - for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { - TSNode child = ts_node_named_child(node, i); - const char *text = csp_is(child, "storage_class_specifier") ? csp_text(ctx, child) : NULL; - if (text && !strcmp(text, "static")) - def->source_flags |= CBM_SOURCE_DECL_STATIC; - } + if (csp_storage_is(ctx, node, "static")) + def->source_flags |= CBM_SOURCE_DECL_STATIC; + int arity = csp_fixed_arity(ctx, decl); + def->source_arity = arity >= 0 ? arity + 1 : 0; size_t module_len = strlen(ctx->module); def->namespace_name = !strcmp(owner, ctx->module) ? "" : !strncmp(owner, ctx->module, module_len) && owner[module_len] == '.' @@ -269,27 +469,112 @@ static void csp_free_function(csp_context_t *ctx, TSNode node, const char *owner def->source_flags |= CBM_SOURCE_DECL_FIXED_ARITY; } -static void csp_method(csp_context_t *ctx, TSNode node, const char *owner, bool is_public) { +static void csp_method(csp_context_t *ctx, TSNode node, const char *owner, uint32_t access) { TSNode decl = csp_function_declarator(node), name_node = csp_field(decl, "declarator"); if (!csp_is(name_node, "field_identifier") && !csp_is(name_node, "identifier")) return; /* Pointer fields, operators and constructors need their own contracts. */ const char *name = csp_text(ctx, name_node); const char *qn = name ? cbm_arena_sprintf(ctx->arena, "%s.%s", owner, name) : NULL; - uint32_t flags = - CBM_SOURCE_DECL_KNOWN | (is_public ? CBM_SOURCE_DECL_PUBLIC : CBM_SOURCE_DECL_PRIVATE); - csp_mark(ctx, node, qn, flags, csp_arity(ctx, decl)); + csp_mark(ctx, node, qn, CBM_SOURCE_DECL_KNOWN | access, csp_arity(ctx, decl)); } -static void csp_class(csp_context_t *ctx, TSNode node, const char *parent) { - TSNode body = csp_field(node, "body"), name_node = csp_field(node, "name"); - if (ts_node_is_null(body) || !csp_is(name_node, "type_identifier")) +/* A name a class body declares. Many have no graph node: a member function declared and + * not defined here, a pure virtual, a nested type, an enumerator, a using-declaration. + * Inside the class such a name still hides anything of that name outside it. */ +static void csp_member_name(csp_context_t *ctx, const char *klass, TSNode name, uint32_t access) { + if (ts_node_is_null(name)) return; - const char *name = csp_text(ctx, name_node); + const char *text = csp_text(ctx, name); + const char *scope_end = text ? strrchr(text, ':') : NULL; + CBMCDecl *fact = csp_add_decl(ctx, CBM_C_DECL_MEMBER_NAME, scope_end ? scope_end + 1 : text, + access == CBM_SOURCE_DECL_PUBLIC ? 0 + : access == CBM_SOURCE_DECL_PROTECTED + ? CBM_C_DECL_NOT_PUBLIC | CBM_C_DECL_PROTECTED + : CBM_C_DECL_NOT_PUBLIC); + if (fact) + fact->type_name = klass; +} + +static void csp_note_member(csp_context_t *ctx, TSNode node, const char *klass, uint32_t access) { + if (csp_is(node, "template_declaration")) { + uint32_t count = ts_node_named_child_count(node); + for (uint32_t i = 0; i < count; i++) + csp_note_member(ctx, ts_node_named_child(node, i), klass, access); + return; + } + if (csp_is(node, "using_declaration")) { + uint32_t count = ts_node_named_child_count(node); + if (count) + csp_member_name(ctx, klass, ts_node_named_child(node, count - 1), access); + return; + } + if (csp_is(node, "alias_declaration")) { + csp_member_name(ctx, klass, csp_field(node, "name"), access); + return; + } + if (csp_is(node, "type_definition")) { + csp_member_name(ctx, klass, csp_field(node, "declarator"), access); + return; + } + TSNode function = csp_function_declarator(node); + if (!ts_node_is_null(function)) { + TSNode name = csp_field(function, "declarator"); + if (csp_is(name, "field_identifier") || csp_is(name, "identifier")) + csp_member_name(ctx, klass, name, access); + return; + } + TSNode type = csp_field(node, "type"); + if (ts_node_is_null(type) || + (!csp_is(type, "struct_specifier") && !csp_is(type, "class_specifier") && + !csp_is(type, "union_specifier") && !csp_is(type, "enum_specifier"))) + return; + csp_member_name(ctx, klass, csp_field(type, "name"), access); + if (!csp_is(type, "enum_specifier")) + return; + TSNode body = csp_field(type, "body"); + uint32_t count = ts_node_is_null(body) ? 0 : ts_node_named_child_count(body); + for (uint32_t i = 0; i < count; i++) { + TSNode enumerator = ts_node_named_child(body, i); + if (csp_is(enumerator, "enumerator")) + csp_member_name(ctx, klass, csp_field(enumerator, "name"), access); + } +} + +/* A struct inside a preprocessor branch stays unknown: unlike a function, whose + * callers cannot compile without it, a branch is how a project swaps one layout + * for another, and member lookup cannot tell which one the includer gets. */ +static void csp_class(csp_context_t *ctx, TSNode node, const char *parent, + const char *typedef_name); + +/* `union vval_union { ... } vval;` defines a type where it declares a member. Extraction + * names it under the outer class or directly under the outer's scope depending on the form, + * and only the name it used has an entry, so the other call finds nothing. */ +static void csp_nested_class(csp_context_t *ctx, TSNode member, const char *outer, + const char *parent) { + TSNode type = csp_field(member, "type"); + if ((!csp_is(type, "struct_specifier") && !csp_is(type, "class_specifier") && + !csp_is(type, "union_specifier")) || + ts_node_is_null(csp_field(type, "body"))) + return; + csp_class(ctx, type, outer, NULL); + csp_class(ctx, type, parent, NULL); +} + +static void csp_class(csp_context_t *ctx, TSNode node, const char *parent, + const char *typedef_name) { + TSNode body = csp_field(node, "body"), name_node = csp_field(node, "name"); + if (ts_node_is_null(body) || (!csp_is(name_node, "type_identifier") && !typedef_name) || + ctx->conditional) + return; + const char *name = + csp_is(name_node, "type_identifier") ? csp_text(ctx, name_node) : typedef_name; const char *qn = name ? cbm_arena_sprintf(ctx->arena, "%s.%s", parent, name) : NULL; if (!qn) { ctx->failed = true; return; } - bool complete = true, is_public = csp_is(node, "struct_specifier"); + bool complete = true, body_complete = true; + uint32_t access = + csp_is(node, "struct_specifier") ? CBM_SOURCE_DECL_PUBLIC : CBM_SOURCE_DECL_PRIVATE; TSTreeCursor header = ts_tree_cursor_new(node); if (ts_tree_cursor_goto_first_child(&header)) do { @@ -305,27 +590,129 @@ static void csp_class(csp_context_t *ctx, TSNode node, const char *parent) { TSNode child = ts_tree_cursor_current_node(&cursor); if (!csp_budget(ctx, 0)) { complete = false; + body_complete = false; break; } if (!ts_node_is_named(child) || csp_is(child, "comment")) continue; if (csp_is(child, "access_specifier")) { - const char *access = csp_text(ctx, child); - is_public = access && !strcmp(access, "public"); + const char *written = csp_text(ctx, child); + access = written && !strcmp(written, "public") ? CBM_SOURCE_DECL_PUBLIC + : written && !strcmp(written, "protected") ? CBM_SOURCE_DECL_PROTECTED + : CBM_SOURCE_DECL_PRIVATE; } else if (csp_is(child, "function_definition") || csp_is(child, "field_declaration") || csp_is(child, "declaration")) { - csp_method(ctx, child, qn, is_public); + csp_method(ctx, child, qn, access); + csp_note_member(ctx, child, name, access); + csp_nested_class(ctx, child, qn, parent); } else { complete = false; + if (csp_is(child, "template_declaration") || csp_is(child, "using_declaration") || + csp_is(child, "alias_declaration") || csp_is(child, "type_definition")) + csp_note_member(ctx, child, name, access); + else if (!csp_is(child, "friend_declaration") && + !csp_is(child, "static_assert_declaration")) + body_complete = false; } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); csp_mark(ctx, node, qn, - CBM_SOURCE_DECL_KNOWN | (complete ? CBM_SOURCE_CLASS_DOMAIN_COMPLETE : 0), -1); + CBM_SOURCE_DECL_KNOWN | (complete ? CBM_SOURCE_CLASS_DOMAIN_COMPLETE : 0) | + (body_complete ? CBM_SOURCE_CLASS_BODY_COMPLETE : 0), + -1); } +static bool csp_preprocessor_branch(TSNode node) { + return csp_is(node, "preproc_if") || csp_is(node, "preproc_ifdef") || + csp_is(node, "preproc_else") || csp_is(node, "preproc_elif") || + csp_is(node, "preproc_elifdef"); +} + +static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigned depth); + +static void csp_walk_children(csp_context_t *ctx, TSNode node, const char *owner, unsigned depth) { + TSTreeCursor cursor = ts_tree_cursor_new(node); + if (ts_tree_cursor_goto_first_child(&cursor)) + do { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (ts_node_is_named(child)) + csp_walk(ctx, child, owner, depth + 1); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + ts_tree_cursor_delete(&cursor); +} + +/* The only code a guard's condition can hide is a header the caller suppressed + * on purpose: declarations inside one exist in every ordinary configuration. + * Any other branch records that its declarations are conditional. */ +static TSNode csp_include_guard(csp_context_t *ctx, TSNode root) { + TSNode guard = {0}; + uint32_t count = ts_node_named_child_count(root); + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_named_child(root, i); + if (csp_is(child, "comment")) + continue; + if (!ts_node_is_null(guard) || !csp_is(child, "preproc_ifdef")) + return (TSNode){0}; + guard = child; + } + if (ts_node_is_null(guard)) + return guard; + TSNode keyword = ts_node_child(guard, 0); + TSNode name = csp_field(guard, "name"); + if (ts_node_is_null(keyword) || !csp_is(name, "identifier") || + ts_node_end_byte(keyword) - ts_node_start_byte(keyword) != 7 || + memcmp(ctx->source + ts_node_start_byte(keyword), "#ifndef", 7) || + !ts_node_is_null(csp_field(guard, "alternative"))) + return (TSNode){0}; + uint32_t kids = ts_node_named_child_count(guard); + for (uint32_t i = 0; i < kids; i++) { + TSNode child = ts_node_named_child(guard, i); + if (csp_is(child, "comment") || ts_node_eq(child, name)) + continue; + const char *defined = + csp_is(child, "preproc_def") ? csp_text(ctx, csp_field(child, "name")) : NULL; + const char *guarded = csp_text(ctx, name); + return defined && guarded && !strcmp(defined, guarded) ? guard : (TSNode){0}; + } + return (TSNode){0}; +} + static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigned depth) { if (!csp_budget(ctx, depth)) return; + if (ctx->facts && (csp_is(node, "namespace_definition") || csp_is(node, "class_specifier") || + csp_is(node, "template_declaration"))) + ctx->facts->cpp_syntax = true; + if (csp_is(node, "preproc_def") || csp_is(node, "preproc_function_def")) { + csp_note_macro(ctx, node); + return; + } + if (csp_preprocessor_branch(node)) { + bool saved = ctx->conditional; + ctx->conditional = true; + csp_walk_children(ctx, node, owner, depth); + ctx->conditional = saved; + return; + } + /* Error recovery around a directive still leaves its neighbours as complete + * nodes. Each is judged by itself, and one with an error inside is skipped. */ + if (csp_is(node, "ERROR")) { + csp_walk_children(ctx, node, owner, depth); + return; + } + if (csp_is(node, "enum_specifier")) { + csp_note_enumerators(ctx, node, owner); + return; + } + if (csp_is(node, "type_definition")) { + TSNode type = csp_field(node, "type"), declarator = csp_field(node, "declarator"); + csp_note_enumerators(ctx, type, owner); + /* An error among the members does not change which struct this is. */ + if (csp_is(type, "class_specifier") || csp_is(type, "struct_specifier") || + csp_is(type, "union_specifier")) + csp_class(ctx, type, owner, + csp_is(declarator, "type_identifier") ? csp_text(ctx, declarator) : NULL); + return; + } if (csp_is(node, "linkage_specification")) { const char *value = csp_text(ctx, csp_field(node, "value")); TSNode body = csp_field(node, "body"); @@ -338,12 +725,17 @@ static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigne return; } if (csp_is(node, "function_definition") || csp_is(node, "declaration")) { - csp_free_function(ctx, node, owner); + /* A definition with an error in its body still has this signature. */ + TSNode declarator = csp_field(node, "declarator"), type = csp_field(node, "type"); + if ((ts_node_is_null(declarator) || !ts_node_has_error(declarator)) && + (ts_node_is_null(type) || !ts_node_has_error(type))) + csp_free_function(ctx, node, owner); if (csp_is(node, "function_definition")) return; } - if (csp_is(node, "class_specifier") || csp_is(node, "struct_specifier")) { - csp_class(ctx, node, owner); + if (csp_is(node, "class_specifier") || csp_is(node, "struct_specifier") || + csp_is(node, "union_specifier")) { + csp_class(ctx, node, owner, NULL); return; } if (csp_is(node, "namespace_definition")) { @@ -361,17 +753,17 @@ static void csp_walk(csp_context_t *ctx, TSNode node, const char *owner, unsigne if (!csp_is(node, "translation_unit") && !csp_is(node, "declaration_list") && !csp_is(node, "declaration")) return; - TSTreeCursor cursor = ts_tree_cursor_new(node); - if (ts_tree_cursor_goto_first_child(&cursor)) - do { - TSNode child = ts_tree_cursor_current_node(&cursor); - if (ts_node_is_named(child)) - csp_walk(ctx, child, owner, depth + 1); - } while (ts_tree_cursor_goto_next_sibling(&cursor)); - ts_tree_cursor_delete(&cursor); + csp_walk_children(ctx, node, owner, depth); } + bool cbm_c_collect_source_facts(CBMArena *arena, const char *source, int length, const char *module, CBMLSPDef *defs, int count) { + return cbm_c_collect_file_facts(arena, source, length, module, CBM_LANG_CPP, defs, count, NULL); +} + +bool cbm_c_collect_file_facts(CBMArena *arena, const char *source, int length, const char *module, + CBMLanguage language, CBMLSPDef *defs, int count, + CBMCFileFacts *facts) { if (count < 0 || (count && !defs)) return false; for (int i = 0; i < count; i++) { @@ -381,11 +773,14 @@ bool cbm_c_collect_source_facts(CBMArena *arena, const char *source, int length, if (!arena || !source || length <= 0 || !module) return false; extern const TSLanguage *tree_sitter_cpp(void); + extern const TSLanguage *tree_sitter_c(void); TSParser *parser = ts_parser_new(); if (!parser) return false; - ts_parser_set_language(parser, tree_sitter_cpp()); - TSTree *tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)length); + /* C source can name a variable `new` or `class`, and the C++ grammar then + * loses the declaration it sits in. */ + ts_parser_set_language(parser, language == CBM_LANG_C ? tree_sitter_c() : tree_sitter_cpp()); + TSTree *tree = cbm_parse_c_source(parser, source, (uint32_t)length, (TSParseOptions){0}); ts_parser_delete(parser); if (!tree) return false; @@ -393,14 +788,23 @@ bool cbm_c_collect_source_facts(CBMArena *arena, const char *source, int length, .source = source, .length = (uint32_t)length, .module = module, + .facts = facts, + .c_language = language == CBM_LANG_C, .definitions = cbm_ht_create(32)}; - ctx.failed = !ctx.definitions || ts_node_has_error(ts_tree_root_node(tree)); - ctx.preprocessor_unknown = cbm_c_preprocessor_domain_unsupported(ts_tree_root_node(tree)); + ctx.failed = !ctx.definitions; + ctx.preprocessor_unknown = + cbm_c_preprocessor_domain_unsupported(ts_tree_root_node(tree), source); + ctx.file_extern_c = strstr(source, "__cplusplus") && strstr(source, "extern \"C\""); + if (facts && !facts->module_qn) + facts->module_qn = module; for (int i = 0; !ctx.failed && i < count; i++) { CBMLSPDef *def = &defs[i]; const char *qn = def->graph_qualified_name ? def->graph_qualified_name : def->qualified_name; - if (!qn || !def->source_start_line || !def->short_name) + /* `static struct config {...} config;` holds a Class and a Variable of one name + * on one line, and only the Class has source facts to receive. */ + if (!qn || !def->source_start_line || !def->short_name || + (def->label && !strcmp(def->label, "Variable"))) continue; size_t base = cbm_qn_callable_base_len_named(qn, def->short_name); const char *key = @@ -420,8 +824,16 @@ bool cbm_c_collect_source_facts(CBMArena *arena, const char *source, int length, if (cbm_ht_get(ctx.definitions, key) != entry) ctx.failed = true; } - if (!ctx.failed) - csp_walk(&ctx, ts_tree_root_node(tree), module, 0); + if (!ctx.failed) { + TSNode root = ts_tree_root_node(tree); + TSNode guard = csp_include_guard(&ctx, root); + if (facts && !ts_node_is_null(guard)) + facts->guard = csp_text(&ctx, csp_field(guard, "name")); + if (ts_node_is_null(guard)) + csp_walk(&ctx, root, module, 0); + else + csp_walk_children(&ctx, guard, module, 0); + } if (ctx.failed) for (int i = 0; i < count; i++) defs[i].source_flags = 0; @@ -429,3 +841,135 @@ bool cbm_c_collect_source_facts(CBMArena *arena, const char *source, int length, ts_tree_delete(tree); return !ctx.failed; } + +static bool csp_append_file(CBMArena *arena, const char ***items, int *count, int *cap, + const char *file) { + if (*count == *cap) { + int next_cap = *cap ? *cap * 2 : 8; + const char **next = cbm_arena_alloc(arena, (size_t)next_cap * sizeof(*next)); + if (!next) + return false; + if (*count) + memcpy(next, *items, (size_t)*count * sizeof(*next)); + *items = next; + *cap = next_cap; + } + (*items)[(*count)++] = file; + return true; +} + +bool cbm_c_facts_add_include(CBMArena *arena, CBMCFileFacts *facts, const char *file) { + return csp_append_file(arena, &facts->includes, &facts->include_count, &facts->include_cap, + file); +} + +bool cbm_c_facts_add_method(CBMArena *arena, CBMCFileFacts *facts, const char *name, + const char *target, const char *owner, int arity) { + csp_context_t ctx = {.arena = arena, .facts = facts, .module = facts->module_qn}; + CBMCDecl *fact = csp_add_decl(&ctx, CBM_C_DECL_METHOD, name, 0); + if (!fact) + return false; + fact->target = target; + fact->type_name = owner; + fact->arity = arity; + return true; +} + +bool cbm_c_facts_add_generated(CBMArena *arena, CBMCFileFacts *facts, const char *file) { + return csp_append_file(arena, &facts->generated_for, &facts->generated_count, + &facts->generated_cap, file); +} + +typedef struct CDeclFileEntry { + const CBMCFileFacts *facts; + struct CDeclFileEntry *next; +} CDeclFileEntry; + +struct CBMCDeclIndex { + CBMCDeclLink **names; + CDeclFileEntry **files; + uint32_t name_mask, file_mask; +}; + +static uint32_t csp_hash(const char *text) { + uint32_t hash = 2166136261u; + for (const unsigned char *p = (const unsigned char *)text; *p; p++) + hash = (hash ^ *p) * 16777619u; + return hash; +} + +static uint32_t csp_bucket_mask(size_t items) { + uint32_t size = 16; + while (size < items * 2 && size < (1u << 28)) + size <<= 1; + return size - 1; +} + +static const char *csp_file_key(const CBMCFileFacts *facts) { + return facts->file_qn ? facts->file_qn : facts->module_qn; +} + +const CBMCDeclIndex *cbm_c_decl_index_build(CBMArena *arena, const CBMLSPDef *defs, int count) { + size_t files = 0, decls = 0; + for (int i = 0; i < count; i++) { + if (!defs[i].c_file_facts) + continue; + files++; + decls += (size_t)defs[i].c_file_facts->decl_count; + } + if (!files) + return NULL; + CBMCDeclIndex *index = cbm_arena_calloc(arena, sizeof(*index)); + if (!index) + return NULL; + index->name_mask = csp_bucket_mask(decls); + index->file_mask = csp_bucket_mask(files); + index->names = cbm_arena_calloc(arena, (size_t)(index->name_mask + 1) * sizeof(*index->names)); + index->files = cbm_arena_calloc(arena, (size_t)(index->file_mask + 1) * sizeof(*index->files)); + if (!index->names || !index->files) + return NULL; + for (int i = 0; i < count; i++) { + const CBMCFileFacts *facts = defs[i].c_file_facts; + if (!facts || !facts->module_qn) + continue; + CDeclFileEntry *entry = cbm_arena_alloc(arena, sizeof(*entry)); + if (!entry) + return NULL; + uint32_t slot = csp_hash(csp_file_key(facts)) & index->file_mask; + *entry = (CDeclFileEntry){.facts = facts, .next = index->files[slot]}; + index->files[slot] = entry; + for (int d = 0; d < facts->decl_count; d++) { + const CBMCDecl *decl = &facts->decls[d]; + uint32_t bucket = csp_hash(decl->name) & index->name_mask; + CBMCDeclLink *link = cbm_arena_alloc(arena, sizeof(*link)); + if (!link) + return NULL; + *link = (CBMCDeclLink){.decl = decl, .next = index->names[bucket]}; + index->names[bucket] = link; + /* `ns.f` is also reachable by the name a call writes. */ + const char *leaf = decl->kind == CBM_C_DECL_PROTOTYPE ? strrchr(decl->name, '.') : NULL; + if (leaf) { + bucket = csp_hash(leaf + 1) & index->name_mask; + link = cbm_arena_alloc(arena, sizeof(*link)); + if (!link) + return NULL; + *link = (CBMCDeclLink){.decl = decl, .next = index->names[bucket]}; + index->names[bucket] = link; + } + } + } + return index; +} + +const CBMCDeclLink *cbm_c_decl_lookup(const CBMCDeclIndex *index, const char *name) { + return index && name ? index->names[csp_hash(name) & index->name_mask] : NULL; +} + +const CBMCFileFacts *cbm_c_file_facts(const CBMCDeclIndex *index, const char *file) { + if (!index || !file) + return NULL; + for (const CDeclFileEntry *e = index->files[csp_hash(file) & index->file_mask]; e; e = e->next) + if (!strcmp(csp_file_key(e->facts), file)) + return e->facts; + return NULL; +} diff --git a/internal/cbm/lsp/c_proof.h b/internal/cbm/lsp/c_proof.h index 24c977827..7df199877 100644 --- a/internal/cbm/lsp/c_proof.h +++ b/internal/cbm/lsp/c_proof.h @@ -8,4 +8,77 @@ bool cbm_c_collect_source_facts(CBMArena *arena, const char *source, int length, const char *module, CBMLSPDef *defs, int count); +/* What a file contributes to the translation units that include it, beyond its + * graph definitions. A prototype, an extern declaration or a macro has no graph + * node, yet a file's declarations decide what a name means in every includer. + * These are source facts: nothing here selects an edge target. */ +typedef enum { + CBM_C_DECL_PROTOTYPE = 0, /* a function declared without a body */ + CBM_C_DECL_MACRO, /* a #define of this name, in any branch */ + CBM_C_DECL_EXTERN_VALUE, /* an extern object declaration */ + CBM_C_DECL_VALUE, /* a graph Variable: object definition or enumerator */ + CBM_C_DECL_METHOD, /* an out-of-line or inline member definition */ + CBM_C_DECL_MEMBER_NAME, /* any name a class body declares; type_name is the class */ +} CBMCDeclKind; + +enum { + CBM_C_DECL_EXTERNAL = 1u << 0, /* external linkage: not static, not an enumerator */ + CBM_C_DECL_C_LINKAGE = 1u << 1, /* written under extern "C" */ + CBM_C_DECL_CONDITIONAL = 1u << 2, /* inside a preprocessor branch */ + CBM_C_DECL_NOT_PUBLIC = 1u << 3, /* a class member under private: or protected: */ + CBM_C_DECL_PROTECTED = 1u << 4, /* with NOT_PUBLIC: reachable from a derived class */ +}; + +typedef struct CBMCDecl { + const char *file; /* the declaring file's identity (see CBMCFileFacts) */ + const char *name; /* namespace-relative name, dotted */ + const char *target; /* VALUE: exact graph QN. Otherwise NULL. */ + const char *type_name; /* VALUE, EXTERN_VALUE: written base type. METHOD: owner's name */ + const CBMType *signature; /* PROTOTYPE: builtin-only audited signature, else NULL */ + int arity; /* PROTOTYPE: fixed parameter count, -1 when not fixed */ + uint32_t flags; + CBMCDeclKind kind; +} CBMCDecl; + +typedef struct CBMCDeclLink { + const CBMCDecl *decl; + struct CBMCDeclLink *next; +} CBMCDeclLink; + +typedef struct CBMCFileFacts { + /* A file's identity is its File QN. A module QN will not do: `sds.h` and `sds.c` + * share one, and an includer of the header does not see the source's statics. */ + const char *file_qn; + const char *module_qn; + const char *guard; /* whole-file include guard macro, or NULL */ + bool plain_header; /* a .h: included by C and C++ alike, so linkage is the includer's */ + bool cpp_syntax; /* a namespace, class or template: this .h is C++ whatever its name */ + CBMCDecl *decls; + int decl_count, decl_cap; + const char **includes; /* proven direct includes, as file identities */ + int include_count, include_cap; + const char **generated_for; /* source files declared through a generated header */ + int generated_count, generated_cap; +} CBMCFileFacts; + +/* As cbm_c_collect_source_facts, also filling facts when it is not NULL. */ +bool cbm_c_collect_file_facts(CBMArena *arena, const char *source, int length, const char *module, + CBMLanguage language, CBMLSPDef *defs, int count, + CBMCFileFacts *facts); + +/* Each adds to facts, copying nothing: strings must outlive the facts. */ +bool cbm_c_facts_add_include(CBMArena *arena, CBMCFileFacts *facts, const char *file); +bool cbm_c_facts_add_generated(CBMArena *arena, CBMCFileFacts *facts, const char *file); +/* A member definition: `size_t StringRef::find(char C) const { ... }` has its class + * in another file, so the class alone cannot reach it. */ +bool cbm_c_facts_add_method(CBMArena *arena, CBMCFileFacts *facts, const char *name, + const char *target, const char *owner, int arity); + +/* Immutable index over every file's facts. Built with the shared registry. */ +typedef struct CBMCDeclIndex CBMCDeclIndex; +const CBMCDeclIndex *cbm_c_decl_index_build(CBMArena *arena, const CBMLSPDef *defs, int count); +/* The bucket holding this name, which may also hold others: compare ->decl->name. */ +const CBMCDeclLink *cbm_c_decl_lookup(const CBMCDeclIndex *index, const char *name); +const CBMCFileFacts *cbm_c_file_facts(const CBMCDeclIndex *index, const char *file); + #endif diff --git a/internal/cbm/lsp/go_lsp.h b/internal/cbm/lsp/go_lsp.h index a1a3c6e7f..0c7d826f9 100644 --- a/internal/cbm/lsp/go_lsp.h +++ b/internal/cbm/lsp/go_lsp.h @@ -125,12 +125,18 @@ typedef struct { uint32_t source_flags; const CBMType *source_return_type; /* Parsed, source-audited annotation only. */ const CBMType *source_signature; /* Audited syntax; type names may be unresolved. */ + int source_arity; /* C family: fixed parameter count + 1; 0 when not fixed. */ /* Go only (#2033): raw aliased type text of `type A = B` ("B"), qualified * by the registrar against def_module_qn like return_types. NULL = not an * identity alias. */ const char *alias_target; + /* C family: one record per file, label CBM_C_FILE_FACTS_LABEL, carrying what + * the file declares for its includers. Registrars ignore the label. */ + const struct CBMCFileFacts *c_file_facts; } CBMLSPDef; +#define CBM_C_FILE_FACTS_LABEL "CFileFacts" + bool cbm_go_collect_source_facts(CBMArena *, const char *, int, const char *, CBMLSPDef *, int); // Parse source, build registry from defs + stdlib, run LSP. diff --git a/internal/cbm/lsp/type_registry.h b/internal/cbm/lsp/type_registry.h index e37097def..20d8d1c8b 100644 --- a/internal/cbm/lsp/type_registry.h +++ b/internal/cbm/lsp/type_registry.h @@ -48,7 +48,12 @@ enum { CBM_SOURCE_DECL_EXTERN_C = 1u << 12, /* Audited fixed parameters with no type, reference, default or unpacking. * PHP may apply them to exact positional values without coercion rules. */ - CBM_SOURCE_DECL_UNTYPED_VALUE_PARAMETERS = 1u << 13 + CBM_SOURCE_DECL_UNTYPED_VALUE_PARAMETERS = 1u << 13, + /* C family: inside a preprocessor branch, so it exists in some configurations. */ + CBM_SOURCE_DECL_CONDITIONAL = 1u << 14, + /* Every name the class body declares is recorded. Unlike DOMAIN_COMPLETE this says + * nothing about base classes, which a lookup searches itself. */ + CBM_SOURCE_CLASS_BODY_COMPLETE = 1u << 15 }; // Registered function/method with full type signature. @@ -80,6 +85,8 @@ typedef struct { * Unqualified source names retain their spelling until the language * adapter proves the declaration's type-name lookup domain. */ const CBMType *source_signature; + int source_arity; /* C family: fixed parameter count + 1; 0 when not fixed. */ + const char *source_file_qn; /* C family: the File that holds this declaration. */ const char *source_namespace; } CBMRegisteredFunc; @@ -116,6 +123,7 @@ typedef struct { bool export_binding_proven; const char *declaration_identity; const char *graph_qualified_name; /* original canonical source graph identity, if different */ + const char *source_file_qn; /* C family: the File that holds this declaration */ uint32_t source_flags; } CBMRegisteredType; @@ -250,6 +258,9 @@ typedef struct CBMTypeRegistry { const char **export_keys; const char **export_targets; int export_count; + + /* C family: what each file declares for its includers (see c_proof.h). */ + const struct CBMCDeclIndex *c_decls; } CBMTypeRegistry; /* Read-only iterator over every exact namespace/name candidate in every diff --git a/internal/cbm/resolve/capabilities.c b/internal/cbm/resolve/capabilities.c index fe398ec09..9f70b5fbb 100644 --- a/internal/cbm/resolve/capabilities.c +++ b/internal/cbm/resolve/capabilities.c @@ -25,11 +25,17 @@ static const edge_row_t edge_rows[] = { "Exact project package paths from the source's module, its go.mod replace directives and " "the enclosing go.work; every other package abstains."}, {CBM_LANG_C, C_EDGES, - "Literal include paths and configured search roots only; unproved preprocessing abstains."}, + "Literal include paths, configured search roots, and a spelling only one project " + "file can satisfy; translation-unit visibility through includes; unproved " + "preprocessing abstains."}, {CBM_LANG_CPP, C_EDGES, - "Literal include paths and configured search roots only; unproved preprocessing abstains."}, + "Literal include paths, configured search roots, and a spelling only one project " + "file can satisfy; translation-unit visibility through includes; unproved " + "preprocessing abstains."}, {CBM_LANG_CUDA, C_EDGES, - "Literal include paths and configured search roots only; unproved preprocessing abstains."}, + "Literal include paths, configured search roots, and a spelling only one project " + "file can satisfy; translation-unit visibility through includes; unproved " + "preprocessing abstains."}, {CBM_LANG_OBJC, EDGE(IMPORTS), "Literal include paths and configured search roots only; no semantic proof bridge " "advertised."}, @@ -248,7 +254,8 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t * out->proof_subsets = CBM_RESOLVE_CAP_SCOPE | CBM_RESOLVE_CAP_IMPORT_EXPORT | CBM_RESOLVE_CAP_DECLARED_TYPE | CBM_RESOLVE_CAP_MEMBER | CBM_RESOLVE_CAP_TYPE_FLOW | CBM_RESOLVE_CAP_CALLABLE; - out->limitation = "Exact visibility and owner proofs; unsupported overload, template, ADL " + out->limitation = "Exact visibility and owner proofs; overloads of one parameter count " + "with separate nodes, templates, ADL with a second visible function " "and callable-field selection abstain."; break; case CBM_LANG_JAVASCRIPT: diff --git a/src/pipeline/import_resolve.h b/src/pipeline/import_resolve.h index e5b718c44..43d9b8b91 100644 --- a/src/pipeline/import_resolve.h +++ b/src/pipeline/import_resolve.h @@ -72,6 +72,12 @@ int cbm_pipeline_rust_import_map(const cbm_gbuf_t *, const char *, const char *, const CBMFileResult *, const char ***, const char ***, int *); /* Exact source include path -> declaration namespace, from the admission * snapshot; dependency File QNs keep their separate extension-bearing identity. */ +/* The File QN of the project source file a generated declaration header is made + * from (`buffer.h.generated.h`, written by the build from `buffer.c`), or NULL. + * The source file sits beside the includer or is the one project file of that + * spelling, as for any include. Caller frees. */ +char *cbm_pipeline_c_generated_source(const cbm_pipeline_ctx_t *ctx, const char *includer, + const char *spelling); int cbm_pipeline_c_include_map(const cbm_gbuf_t *, const char *, const char *, const CBMFileResult *, const char ***, const char ***, int *); #endif diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 59712b80b..1a2f003c4 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -671,7 +671,9 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch } } dst->lang = lang; - dst->alias_target = (lang == CBM_LANG_GO && src->alias_target) + dst->alias_target = ((lang == CBM_LANG_GO || lang == CBM_LANG_C || lang == CBM_LANG_CPP || + lang == CBM_LANG_CUDA) && + src->alias_target) ? cbm_arena_strdup(arena, src->alias_target) : NULL; dst->decorators = NULL; @@ -1131,6 +1133,75 @@ static int pxc_build_py_namespace_records(CBMArena *arena, const CBMFileResult * return written; } +static bool pxc_is_c_family(CBMLanguage language) { + return language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA; +} + +/* Source facts for one C-family file's definitions, and one more record: what the + * file declares for its includers, and which project files it includes. Those + * includes are the proven ones, so an include this index cannot place stops the + * chain there rather than guessing where it leads. */ +static void pxc_collect_c_file(const cbm_pipeline_ctx_t *ctx, CBMArena *arena, const char *source, + int source_length, CBMFileResult *fr, const cbm_file_info_t *file, + CBMLSPDef *defs, int *idx, int room, int file_start) { + int count = *idx - file_start; + CBMCFileFacts *facts = ctx ? cbm_arena_calloc(arena, sizeof(*facts)) : NULL; + const char *module = cbm_arena_strdup(arena, fr->module_qn); + if (facts) { + size_t path_len = strlen(file->rel_path); + facts->plain_header = path_len > 2 && !strcmp(file->rel_path + path_len - 2, ".h"); + facts->module_qn = module; + char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, file->rel_path, "__file__"); + facts->file_qn = file_qn ? cbm_arena_strdup(arena, file_qn) : NULL; + free(file_qn); + } + (void)cbm_c_collect_file_facts(arena, source, source_length, module, file->language, defs, + count, facts); + if (!facts || count >= room) + return; + for (int i = 0; i < count; i++) { + const CBMLSPDef *def = &defs[i]; + const char *target = def->graph_qualified_name; + if (!def->label || strcmp(def->label, "Method") || !def->short_name || !target) + continue; + /* The name written before the member: `K` of `M.ns.K.m`. */ + size_t length = strlen(target), leaf = strlen(def->short_name); + const char *owner = NULL; + if (length > leaf + 1 && target[length - leaf - 1] == '.') { + size_t end = length - leaf - 1, start = end; + while (start > 0 && target[start - 1] != '.') + start--; + owner = cbm_arena_strndup(arena, target + start, end - start); + } + (void)cbm_c_facts_add_method(arena, facts, def->short_name, target, owner, + def->signature_param_types ? def->signature_param_count : -1); + } + const cbm_resolve_import_snapshot_t *snapshot = + cbm_pipeline_import_snapshot(ctx->gbuf, ctx->project_name, file->rel_path, fr); + for (size_t i = 0; snapshot && i < snapshot->count; i++) { + const cbm_resolve_import_fact_t *fact = &snapshot->items[i]; + if (cbm_resolve_admit(&fact->dependency) && cbm_resolve_admit(&fact->binding) && + fact->dependency.target) { + (void)cbm_c_facts_add_include(arena, facts, + cbm_arena_strdup(arena, fact->dependency.target)); + continue; + } + char *generated = cbm_pipeline_c_generated_source(ctx, file->rel_path, fact->module_path); + if (generated) + (void)cbm_c_facts_add_generated(arena, facts, cbm_arena_strdup(arena, generated)); + free(generated); + } + CBMLSPDef *record = &defs[count]; + memset(record, 0, sizeof(*record)); + record->qualified_name = cbm_arena_sprintf(arena, "%s.@c-file-facts", module); + record->short_name = "@c-file-facts"; + record->label = CBM_C_FILE_FACTS_LABEL; + record->def_module_qn = module; + record->lang = file->language; + record->c_file_facts = facts; + (*idx)++; +} + /* Collect a project-wide CBMLSPDef[] from all cached results. Returns a * malloc'd array (caller frees) of length *out_count. String fields are * borrowed from cache[i]->arena and from def_modules[i] (also borrowed). @@ -1163,6 +1234,9 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMArena *are } } total += defs; + if (ctx && pxc_is_c_family(files[i].language)) { + total += 1; /* the file's declaration facts */ + } if (files[i].language == CBM_LANG_RUST) { total += impls; } @@ -1305,8 +1379,8 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMArena *are } if (files[fi].language == CBM_LANG_C || files[fi].language == CBM_LANG_CPP || files[fi].language == CBM_LANG_CUDA) - (void)cbm_c_collect_source_facts(arena, source, source_length, fr->module_qn, - defs + file_start, idx - file_start); + pxc_collect_c_file(ctx, arena, source, source_length, fr, &files[fi], + defs + file_start, &idx, capacity - file_start, file_start); else if (files[fi].language == CBM_LANG_GO) (void)cbm_go_collect_source_facts(arena, source, source_length, def_modules[fi], defs + file_start, idx - file_start); @@ -1955,6 +2029,9 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca if (!dst_calls || !src_out) return; + /* Every proof of this file comes through here. Its size follows the file, and proof + * work counts hashed bytes, so a cap silently cost a large file its last rows, at a + * point that moved with the length of the checkout path. */ cbm_resolve_proof_pool_t proof_pool; cbm_resolve_proof_pool_init_project(&proof_pool, dst_arena); cbm_resolve_proof_clone_session_t proofs; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index f05f5f291..6fdda2114 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -4490,12 +4490,18 @@ static void resolve_worker_impl(int worker_id, void *ctx_ptr) { 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, rel); + /* C and C++ values, fields and written types bind in the translation unit as + * well, so a file with no call left to resolve can still hold those sites. The + * sequential route has no such filter and the two routes must agree. */ + bool c_family_sites = + (lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) && + (result->usages.count > 0 || result->rw.count > 0 || result->type_refs.count > 0); 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)) || - unowned_c_member) && + unowned_c_member || c_family_sites) && !is_generated) || result->has_test_definition_owners; diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index a0d4374b1..bc6d1779c 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -2306,9 +2306,225 @@ static const cbm_gbuf_node_t *include_in_directory(const cbm_pipeline_ctx_t *ctx return cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, canonical + size + 1); } +/* Headers the toolchain supplies. A project file spelled the same way is a + * compat shim or a vendored copy, and nothing here says the compiler would + * choose it over the toolchain's own, so these spellings never infer a target. + * Spellings under a system directory are matched by prefix in the caller. */ +static bool include_toolchain_spelling(const char *spelling) { + static const char *const names[] = { + "algorithm", + "alloca.h", + "any", + "arpa/inet.h", + "array", + "assert.h", + "atomic", + "bit", + "bitset", + "cassert", + "cctype", + "cerrno", + "cfloat", + "charconv", + "chrono", + "cinttypes", + "climits", + "clocale", + "cmath", + "codecvt", + "compare", + "complex", + "complex.h", + "concepts", + "condition_variable", + "coroutine", + "csetjmp", + "csignal", + "cstdarg", + "cstddef", + "cstdint", + "cstdio", + "cstdlib", + "cstring", + "ctime", + "ctype.h", + "cuchar", + "cwchar", + "cwctype", + "deque", + "dirent.h", + "dlfcn.h", + "errno.h", + "exception", + "execinfo.h", + "execution", + "expected", + "fcntl.h", + "fenv.h", + "filesystem", + "float.h", + "fnmatch.h", + "format", + "forward_list", + "fstream", + "functional", + "future", + "getopt.h", + "glob.h", + "grp.h", + "initializer_list", + "inttypes.h", + "iomanip", + "ios", + "iosfwd", + "iostream", + "iso646.h", + "istream", + "iterator", + "langinfo.h", + "latch", + "libgen.h", + "limits", + "limits.h", + "list", + "locale", + "locale.h", + "malloc.h", + "map", + "math.h", + "memory", + "memory_resource", + "mutex", + "net/if.h", + "netdb.h", + "netinet/in.h", + "netinet/tcp.h", + "new", + "numbers", + "numeric", + "optional", + "ostream", + "paths.h", + "poll.h", + "pthread.h", + "pwd.h", + "queue", + "random", + "ranges", + "ratio", + "regex", + "regex.h", + "resolv.h", + "sched.h", + "scoped_allocator", + "search.h", + "semaphore", + "semaphore.h", + "set", + "setjmp.h", + "shared_mutex", + "signal.h", + "source_location", + "span", + "spawn.h", + "sstream", + "stack", + "stdalign.h", + "stdarg.h", + "stdatomic.h", + "stdbool.h", + "stddef.h", + "stdexcept", + "stdint.h", + "stdio.h", + "stdlib.h", + "stdnoreturn.h", + "stop_token", + "streambuf", + "string", + "string.h", + "string_view", + "strings.h", + "syncstream", + "syslog.h", + "system_error", + "termios.h", + "tgmath.h", + "thread", + "threads.h", + "time.h", + "tuple", + "type_traits", + "typeindex", + "typeinfo", + "uchar.h", + "unistd.h", + "unordered_map", + "unordered_set", + "utility", + "utime.h", + "valarray", + "variant", + "vector", + "version", + "wchar.h", + "wctype.h", + "windows.h", + "winsock2.h", + "ws2tcpip.h", + }; + /* Autoconf's output, under the build tree of whichever component is compiling. + * Another component's own config.h is the classic false match for it. */ + if (!strcmp(spelling, "config.h")) + return true; + if (!strncmp(spelling, "sys/", 4) || !strncmp(spelling, "bits/", 5)) + return true; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) + if (!strcmp(spelling, names[i])) + return true; + return false; +} + +/* With no build configuration for this file, the include's whole spelling is + * the remaining evidence: exactly one project file must end in that path, at a + * path-component boundary. That file is what a -I at its prefix selects, and no + * other project file could be selected by any -I. Several such files is a real + * collision, reported as ambiguous and never settled by nearness. */ +static const cbm_gbuf_node_t *resolve_inferred_include(const cbm_pipeline_ctx_t *ctx, + const CBMImport *imp, CBMHashTable *map, + bool *ambiguous, const char **reason) { + const char *spelling = imp->module_path; + *reason = "include_search_roots_unknown"; + if (!map || !spelling[0] || spelling[0] == '/' || strchr(spelling, '\\') || + strstr(spelling, "//") || !strncmp(spelling, "./", 2) || !strncmp(spelling, "../", 3) || + strstr(spelling, "/../") || strstr(spelling, "/./")) + return NULL; + if (include_toolchain_spelling(spelling)) { + *reason = "include_toolchain_header"; + return NULL; + } + char key[PKGMAP_PATH_BUF]; + int length = snprintf(key, sizeof(key), "@cbm:incpath:%s", spelling); + if (length <= 0 || (size_t)length >= sizeof(key)) + return NULL; + const char *rel = cbm_ht_get(map, key); + if (!rel) { + *reason = "include_not_in_project"; + return NULL; + } + if (!strcmp(rel, ambiguous_pkgmap_value)) { + *ambiguous = true; + *reason = "include_path_ambiguous"; + return NULL; + } + *reason = "include_unique_project_path"; + return cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, rel); +} + static const cbm_gbuf_node_t *resolve_configured_include(const cbm_pipeline_ctx_t *ctx, const char *source_rel, - const CBMImport *imp, CBMHashTable *map) { + const CBMImport *imp, CBMHashTable *map, + bool *ambiguous, const char **reason) { if (!source_rel || (imp->kind != CBM_IMPORT_KIND_INCLUDE_QUOTED && imp->kind != CBM_IMPORT_KIND_INCLUDE_SYSTEM)) return NULL; @@ -2338,7 +2554,12 @@ static const cbm_gbuf_node_t *resolve_configured_include(const cbm_pipeline_ctx_ int length = snprintf(key, sizeof(key), "@cbm:cc:%s", source_rel); const char *roots = map && length > 0 && (size_t)length < sizeof(key) ? cbm_ht_get(map, key) : NULL; - if (!roots || strcmp(roots, ambiguous_pkgmap_value) == 0) + /* A compile entry is authoritative for this file, including when it names + * no root that holds the header: nothing is inferred past configuration. */ + if (!roots) + return resolve_inferred_include(ctx, imp, map, ambiguous, reason); + *reason = "configured_include_root"; + if (strcmp(roots, ambiguous_pkgmap_value) == 0) return NULL; for (const char *p = roots; p && p[0];) { const char *end = strchr(p, '\n'); @@ -2641,9 +2862,32 @@ static bool rust_project_build(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *f return true; } +/* Every component-boundary suffix of every C-family file path, for the inferred + * include rule. A suffix shared by two files becomes the ambiguous marker. */ +static bool build_include_path_context(const cbm_file_info_t *files, int count, CBMHashTable *map) { + for (int i = 0; i < count; i++) { + CBMLanguage language = files[i].language; + if ((language != CBM_LANG_C && language != CBM_LANG_CPP && language != CBM_LANG_CUDA && + language != CBM_LANG_OBJC) || + !files[i].rel_path) + continue; + for (const char *suffix = files[i].rel_path; suffix;) { + char key[PKGMAP_PATH_BUF]; + int length = snprintf(key, sizeof(key), "@cbm:incpath:%s", suffix); + if (length <= 0 || (size_t)length >= sizeof(key) || + !import_fact_put(map, key, files[i].rel_path)) + return false; + const char *slash = strchr(suffix, '/'); + suffix = slash ? slash + 1 : NULL; + } + } + return true; +} + static bool import_context_build(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int count, CBMFileResult **cache, CBMHashTable *map) { - if (!ctx || !map || !build_compile_import_context(ctx, map)) + if (!ctx || !map || !build_compile_import_context(ctx, map) || + !build_include_path_context(files, count, map)) return false; return rust_project_build(ctx, files, count, cache); } @@ -4104,7 +4348,7 @@ static const cbm_gbuf_node_t *resolve_import_target(const cbm_pipeline_ctx_t *ct lang == CBM_LANG_OBJC) { *reason = imp->kind == CBM_IMPORT_KIND_INCLUDE_QUOTED ? "quoted_include_path" : "include_search_roots_unknown"; - return resolve_configured_include(ctx, source_rel, imp, namespace_map); + return resolve_configured_include(ctx, source_rel, imp, namespace_map, ambiguous, reason); } if (lang == CBM_LANG_RUST) { return resolve_rust_import(ctx, source_rel, imp, ambiguous, NULL, reason); @@ -5683,6 +5927,33 @@ int cbm_pipeline_rust_import_map(const cbm_gbuf_t *graph, const char *project, out_count); } +char *cbm_pipeline_c_generated_source(const cbm_pipeline_ctx_t *ctx, const char *includer, + const char *spelling) { + static const char suffix[] = ".h.generated.h"; + size_t length = spelling ? strlen(spelling) : 0, suffix_len = sizeof(suffix) - 1; + CBMHashTable *map = cbm_pipeline_get_nsmap(); + if (!ctx || !map || !includer || length <= suffix_len || + strcmp(spelling + length - suffix_len, suffix)) + return NULL; + char stem[PKGMAP_PATH_BUF], beside[PKGMAP_PATH_BUF], key[PKGMAP_PATH_BUF]; + int written = snprintf(stem, sizeof(stem), "%.*s.c", (int)(length - suffix_len), spelling); + if (written <= 0 || (size_t)written >= sizeof(stem)) + return NULL; + char *directory = path_dirname(includer); + bool near = directory && cbm_repo_rel_join(directory, stem, beside, sizeof(beside)) && + cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, beside); + cbm_free(CBM_MEM_CLASS_SEMANTIC, directory); + if (near) + return cbm_pipeline_fqn_compute(ctx->project_name, beside, "__file__"); + written = snprintf(key, sizeof(key), "@cbm:incpath:%s", stem); + if (written <= 0 || (size_t)written >= sizeof(key)) + return NULL; + const char *rel = cbm_ht_get(map, key); + if (!rel || !strcmp(rel, ambiguous_pkgmap_value)) + return NULL; + return cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__"); +} + int cbm_pipeline_c_include_map(const cbm_gbuf_t *graph, const char *project, const char *source_rel, const CBMFileResult *result, const char ***out_keys, const char ***out_values, int *out_count) { diff --git a/src/pipeline/pass_resolver.c b/src/pipeline/pass_resolver.c index b9ad17f2d..afdde122a 100644 --- a/src/pipeline/pass_resolver.c +++ b/src/pipeline/pass_resolver.c @@ -100,6 +100,10 @@ typedef struct { /* cfg twins and one type's repeated impl methods are one graph identity; * the Rust adapter separately rejects a QN two scopes of a file claim. */ bool rust; + /* A C file's #if branches define one name more than once. The graph holds one + * node for them, with each branch's span as a variant, so an edge to or from + * that node is true of the configurations it was found in. */ + bool c_variants; } target_proof_t; typedef struct { @@ -394,6 +398,11 @@ static bool add_target(cbm_shared_resolver_t *resolver, const CBMDefinition *def bool same_span = prior->start_line == definition->start_line && prior->end_line == definition->end_line; prior->cross_file |= strcmp(prior->file, file) != 0; + if (same_owner && + (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA)) { + prior->c_variants = true; + return true; + } if (same_owner && !same_span && cbm_oo_language(language) && (strcmp(definition->label, "Class") != 0 || language == CBM_LANG_CSHARP)) prior->overload_set = true; @@ -568,8 +577,9 @@ static bool target_materialized(const file_resolver_t *resolver, const cbm_gbuf_ return true; if (strcmp(proof->label, node->label)) return false; - return proof->overload_set || (proof->start_line == (uint32_t)node->start_line && - proof->end_line == (uint32_t)node->end_line); + return proof->overload_set || proof->c_variants || + (proof->start_line == (uint32_t)node->start_line && + proof->end_line == (uint32_t)node->end_line); } /* JavaScript nests functions freely and extraction names them flat, so two @@ -3695,6 +3705,11 @@ static cbm_resolve_result_t generic_result(file_resolver_t *resolver, const char : proof; } } + /* The C adapter's semantic rows are the whole translation-unit lookup. With no + * row there is no second, name-based way to arrive at a target. */ + if (file->language == CBM_LANG_C || file->language == CBM_LANG_CPP || + file->language == CBM_LANG_CUDA) + return unresolved("c_translation_unit_binding_unproven"); if (file->language == CBM_LANG_DART) { cbm_resolve_result_t proof = dart_name_result(resolver, name); return cbm_resolve_binding_validate(resolver->bindings, name, start, end, origin, proof); @@ -4203,6 +4218,12 @@ static bool resolve_writes(file_resolver_t *resolver, CBMArena *arena) { const semantic_site_t *site = find_semantic(resolver, rw->enclosing_func_qn, rw->site_start_byte, rw->site_end_byte, rw->source_origin, CBM_RESOLVED_FIELD_REFERENCE); + if (!site && !rw->is_member_access && + (resolver->file->language == CBM_LANG_C || resolver->file->language == CBM_LANG_CPP || + resolver->file->language == CBM_LANG_CUDA)) + site = + find_semantic(resolver, rw->enclosing_func_qn, rw->site_start_byte, + rw->site_end_byte, rw->source_origin, CBM_RESOLVED_VALUE_REFERENCE); cbm_resolve_result_t outcome; if (site) outcome = semantic_result(resolver, site, arena); diff --git a/tests/test_conditional_variants.c b/tests/test_conditional_variants.c index 3cd2d6d94..e18fe0138 100644 --- a/tests/test_conditional_variants.c +++ b/tests/test_conditional_variants.c @@ -212,25 +212,25 @@ static const cv_golden_t cv_goldens[] = { {"cv_c_preprocessor", "cv_c_preprocessor-2", "pick.c", "pick", "beta()", 0, "pick.c", "beta", 2, "void beta(void) {}"}, {"cv_c_preprocessor", "cv_c_preprocessor-3", "pick.c", "caller", "pick()", 0, "pick.c", "pick", - 4, "void pick(void) { alpha(); }"}, + 6, "void pick(void) { beta(); }"}, {"cv_cpp_elif", "cv_cpp_elif-1", "pick.cpp", "pick", "alpha()", 1, "pick.cpp", "alpha", 1, "void alpha() {}"}, {"cv_cpp_elif", "cv_cpp_elif-2", "pick.cpp", "pick", "beta()", 1, "pick.cpp", "beta", 2, "void beta() {}"}, - {"cv_cpp_elif", "cv_cpp_elif-3", "pick.cpp", "caller", "pick()", 2, "pick.cpp", "pick", 4, - "void pick() { alpha(); }"}, + {"cv_cpp_elif", "cv_cpp_elif-3", "pick.cpp", "caller", "pick()", 2, "pick.cpp", "pick", 6, + "void pick() { beta(); }"}, {"cv_cpp_class_members", "cv_cpp_class_members-1", "pick.cpp", "pick", "alpha()", 1, "pick.cpp", "alpha", 2, " void alpha() {}"}, {"cv_cpp_class_members", "cv_cpp_class_members-2", "pick.cpp", "pick", "beta()", 1, "pick.cpp", "beta", 3, " void beta() {}"}, {"cv_cpp_class_members", "cv_cpp_class_members-3", "pick.cpp", "caller", "pick()", 2, - "pick.cpp", "pick", 5, " void pick() { alpha(); }"}, + "pick.cpp", "pick", 7, " void pick() { beta(); }"}, {"cv_cuda", "cv_cuda-1", "pick.cu", "pick", "alpha()", 1, "pick.cu", "alpha", 1, "__device__ void alpha() {}"}, {"cv_cuda", "cv_cuda-2", "pick.cu", "pick", "beta()", 1, "pick.cu", "beta", 2, "__device__ void beta() {}"}, - {"cv_cuda", "cv_cuda-3", "pick.cu", "caller", "pick()", 2, "pick.cu", "pick", 4, - "__device__ void pick() { alpha(); }"}, + {"cv_cuda", "cv_cuda-3", "pick.cu", "caller", "pick()", 2, "pick.cu", "pick", 6, + "__device__ void pick() { beta(); }"}, {"cv_csharp_class_members", "cv_csharp_class_members-1", "Picker.cs", "Pick", "Alpha()", 1, "Picker.cs", "Alpha", 2, " static void Alpha() {}"}, {"cv_csharp_class_members", "cv_csharp_class_members-2", "Picker.cs", "Pick", "Beta()", 1, @@ -370,6 +370,20 @@ static const cv_recovered_t cv_recovered[] = { {"cv_rust_cfg_methods-1", 6, 6, " fn pick(&self) { alpha(); }", NULL}, {"cv_rust_cfg_methods-2", 8, 8, " fn pick(&self) { beta(); }", NULL}, {"cv_rust_cfg_methods-3", 0, 0, NULL, "Method"}, + /* The translation-unit rules bind a function's body in either branch, and a caller to the + * one node both branches share. That node's stored span is the last branch. */ + {"cv_c_preprocessor-1", 4, 4, "void pick(void) { alpha(); }", NULL}, + {"cv_c_preprocessor-2", 6, 6, "void pick(void) { beta(); }", NULL}, + {"cv_c_preprocessor-3", 0, 0, NULL, NULL}, + {"cv_cpp_elif-1", 4, 4, "void pick() { alpha(); }", NULL}, + {"cv_cpp_elif-2", 6, 6, "void pick() { beta(); }", NULL}, + {"cv_cpp_elif-3", 0, 0, NULL, NULL}, + {"cv_cpp_class_members-1", 5, 5, " void pick() { alpha(); }", "Method"}, + {"cv_cpp_class_members-2", 7, 7, " void pick() { beta(); }", "Method"}, + {"cv_cpp_class_members-3", 0, 0, NULL, "Method"}, + {"cv_cuda-1", 4, 4, "__device__ void pick() { alpha(); }", NULL}, + {"cv_cuda-2", 6, 6, "__device__ void pick() { beta(); }", NULL}, + {"cv_cuda-3", 0, 0, NULL, NULL}, }; static bool cv_quality(cv_proj_t *p, const char *test, const cv_file_t *files, int count) { @@ -447,8 +461,7 @@ static bool cv_quality(cv_proj_t *p, const char *test, const cv_file_t *files, i .why = recovered ? "This source-authored body call is recovered and must retain its exact " - "callable target; merged Python and Rust callers also require their " - "authored variant." + "callable target; merged callers also require their authored variant." : g->target ? "The strict resolver has no admitted binding for these conditional or " "build-selected declarations; each source relationship remains recall debt." diff --git a/tests/test_edge_imports.c b/tests/test_edge_imports.c index 6ad6aa6f1..f13a4eaa1 100644 --- a/tests/test_edge_imports.c +++ b/tests/test_edge_imports.c @@ -3266,6 +3266,123 @@ TEST(ei_strict_rust_ambiguous_declared_module_is_not_ranked) { PASS(); } +/* The IMPORTS targets of one file, as a count and whether target_file is among them. */ +static int ei_import_count(cbm_store_t *store, const char *project, const char *source_file, + const char *target_file) { + int64_t source = ei_node_id_for_file_label(store, project, source_file, "File"); + int64_t expected = target_file ? ei_node_id_for_file_label(store, project, target_file, "File") + : 0; + if (source <= 0 || (target_file && expected <= 0)) + return -1; + cbm_edge_t *edges = NULL; + int count = 0; + if (cbm_store_find_edges_by_source_type(store, source, "IMPORTS", &edges, &count) != + CBM_STORE_OK) + return -1; + int matched = 0; + for (int i = 0; i < count; i++) + matched += edges[i].target_id == expected; + int total = count; + cbm_store_free_edges(edges, count); + return target_file ? (matched == 1 && total == 1 ? 1 : -1) : total; +} + +/* No build configuration: an include spelled so that exactly one project file ends + * in that path, at a component boundary, names that file. */ +TEST(ei_c_unique_include_spelling_binds_its_one_project_file) { + static const EILangFile files[] = { + {"include/suite.h", "int suite(void);\n"}, + {"src/state.c", "#include \"suite.h\"\n"}, + {"src/nvim/api/buffer.h", "int api(void);\n"}, + {"src/nvim/buffer.c", "#include \"nvim/api/buffer.h\"\n"}, + {"include/linux/mm.h", "int mm(void);\n"}, + {"drivers/net/rx.c", "#include \n"}, + }; + EILangProj lp; + cbm_store_t *store = ei_index_files(&lp, files, 6); + ASSERT_NOT_NULL(store); + int quoted = ei_import_count(store, lp.project, "src/state.c", "include/suite.h"); + int rooted = ei_import_count(store, lp.project, "src/nvim/buffer.c", "src/nvim/api/buffer.h"); + int angle = ei_import_count(store, lp.project, "drivers/net/rx.c", "include/linux/mm.h"); + ei_cleanup(&lp, store); + ASSERT_EQ(quoted, 1); + ASSERT_EQ(rooted, 1); + ASSERT_EQ(angle, 1); + PASS(); +} + +TEST(ei_c_include_spelling_collision_abstains_honestly) { + static const EILangFile files[] = { + {"src/sds.h", "int a(void);\n"}, + {"deps/hiredis/sds.h", "int b(void);\n"}, + {"src/sds.c", "#include \"sds.h\"\n"}, + {"deps/hiredis/sds.c", "#include \"sds.h\"\n"}, + {"modules/m.c", "#include \"sds.h\"\n"}, + }; + EILangProj lp; + cbm_store_t *store = ei_index_files(&lp, files, 5); + ASSERT_NOT_NULL(store); + /* Beside the includer the search is exact, whatever else shares the name. */ + int beside = ei_import_count(store, lp.project, "src/sds.c", "src/sds.h"); + int vendored = ei_import_count(store, lp.project, "deps/hiredis/sds.c", "deps/hiredis/sds.h"); + /* Elsewhere nothing says which of the two a -I would select, however near. */ + int elsewhere = ei_import_count(store, lp.project, "modules/m.c", NULL); + ei_cleanup(&lp, store); + ASSERT_EQ(beside, 1); + ASSERT_EQ(vendored, 1); + ASSERT_EQ(elsewhere, 0); + PASS(); +} + +TEST(ei_c_toolchain_header_never_binds_a_project_namesake) { + static const EILangFile files[] = { + {"compat/string.h", "int a(void);\n"}, + {"compat/sys/types.h", "int b(void);\n"}, + {"compat/stdio.h", "int c(void);\n"}, + {"src/main.c", + "#include \n#include \n#include \"stdio.h\"\nint main(void);\n"}, + }; + EILangProj lp; + cbm_store_t *store = ei_index_files(&lp, files, 4); + ASSERT_NOT_NULL(store); + int count = ei_import_count(store, lp.project, "src/main.c", NULL); + ei_cleanup(&lp, store); + ASSERT_EQ(count, 0); + PASS(); +} + +TEST(ei_c_partial_or_relative_spelling_never_matches_by_suffix) { + static const EILangFile files[] = { + {"src/buffer.h", "int a(void);\n"}, + {"src/main.c", "#include \"uffer.h\"\n#include \"../other/buffer.h\"\n" + "#include \"src/../src/buffer.h\"\n"}, + }; + EILangProj lp; + cbm_store_t *store = ei_index_files(&lp, files, 2); + ASSERT_NOT_NULL(store); + int count = ei_import_count(store, lp.project, "src/main.c", NULL); + ei_cleanup(&lp, store); + ASSERT_EQ(count, 0); + PASS(); +} + +/* A compile entry says where this file searches. A project file those roots do not + * reach is not selected, even when its spelling would be unique. */ +TEST(ei_c_configured_roots_stop_inference) { + static const EILangFile files[] = { + {"main.cpp", "#include \nint main() { return 0; }\n"}, + {"elsewhere/target.h", "int a();\n"}, + {"user/unrelated.h", "int b();\n"}, + }; + EILangProj lp; + cbm_store_t *store = ei_index_files_configured(&lp, files, 3, "-I user"); + ASSERT_NOT_NULL(store); + int count = ei_import_count(store, lp.project, "main.cpp", NULL); + ei_cleanup(&lp, store); + ASSERT_EQ(count, 0); + PASS(); +} + TEST(ei_strict_configured_include_group_order) { static const EILangFile files[] = { {"main.cpp", "#include \n#include \"quote.h\"\nint main() { return 0; }\n"}, @@ -3342,6 +3459,11 @@ SUITE(edge_imports) { RUN_TEST(ei_strict_rust_custom_crate_root_and_decoy); RUN_TEST(ei_strict_rust_ambiguous_declared_module_is_not_ranked); RUN_TEST(ei_strict_configured_include_group_order); + RUN_TEST(ei_c_unique_include_spelling_binds_its_one_project_file); + RUN_TEST(ei_c_include_spelling_collision_abstains_honestly); + RUN_TEST(ei_c_toolchain_header_never_binds_a_project_namesake); + RUN_TEST(ei_c_partial_or_relative_spelling_never_matches_by_suffix); + RUN_TEST(ei_c_configured_roots_stop_inference); RUN_TEST(ei_strict_quoted_include_exact_target_and_unmodeled_system_gap); RUN_TEST(ei_strict_bare_es_package_does_not_bind_root_module); diff --git a/tests/test_lsp_resolution_probe.c b/tests/test_lsp_resolution_probe.c index e5b04f32f..528c5a6d8 100644 --- a/tests/test_lsp_resolution_probe.c +++ b/tests/test_lsp_resolution_probe.c @@ -747,77 +747,47 @@ static const LRP_Golden LRP_GOLDEN[] = { "The visible external prototype and the unique project definition have identical builtin " "signatures and C linkage. The source-audited external-definition bridge supports this " "cross-file call; prototype materialization is not required."}, - {"lrp_c_s2_funcptr_in_struct", "main.c", "run", "make_ops()", 0, "main.c", "make_ops", "CALLS", - 2, "Ops make_ops(void);", false, false, - "The visible prototype is a source declaration, but C extraction registers only function " - "definitions; no graph node or exact registry declaration exists for the prototype. Cross-TU " - "definition equivalence is a separate gap requiring an explicit linkage unit and compatible " - "effective linkage/signature.", - "Reviewed lang_specs.c c_func_types, extract_defs.c resolve_func_name_c_family, c_lsp.c " - "c_proof_collect/c_proof_domain; Source audit reported binding_not_found. Existing " - "resolver_c " - "source-prototype unit controls inject declaration identities and therefore do not test " - "materialization."}, - {"lrp_c_s3_constructor", "main.c", "make_buf", "buf_new(n)", 0, "main.c", "buf_new", "CALLS", 2, - "Buf *buf_new(int cap);", false, false, - "The visible prototype is a source declaration, but C extraction registers only function " - "definitions; no graph node or exact registry declaration exists for the prototype. Cross-TU " - "definition equivalence is a separate gap requiring an explicit linkage unit and compatible " - "effective linkage/signature.", - "Reviewed lang_specs.c c_func_types, extract_defs.c resolve_func_name_c_family, c_lsp.c " - "c_proof_collect/c_proof_domain; Source audit reported binding_not_found. Existing " - "resolver_c " - "source-prototype unit controls inject declaration identities and therefore do not test " - "materialization."}, + {"lrp_c_s2_funcptr_in_struct", "main.c", "run", "make_ops()", 0, "ops.c", "make_ops", "CALLS", 5, + "Ops make_ops(void) {", true, false, + "A prototype in the calling file declares the external name, and the one definition of " + "that name in the project is the target.", + "The earlier golden named the prototype, which has no graph node, and recorded a gap. The " + "translation-unit rules bind the definition, so the golden now names it."}, + {"lrp_c_s3_constructor", "main.c", "make_buf", "buf_new(n)", 0, "buf.c", "buf_new", "CALLS", 4, + "Buf *buf_new(int cap) {", true, false, + "A prototype in the calling file declares the external name, and the one definition of " + "that name in the project is the target.", + "The earlier golden named the prototype, which has no graph node, and recorded a gap. The " + "translation-unit rules bind the definition, so the golden now names it."}, {"lrp_c_s4_static_local", "main.c", "run", "exported(n)", 0, "util.c", "exported", "CALLS", 3, "int exported(int x) { return helper(x); }", true, false, NULL, "The visible external prototype binds the unique compatible external definition in util.c. " "The separate same-file helper call remains required, and main.run to the static helper " "remains explicitly forbidden."}, - {"lrp_c_s5_chained_deref", "main.c", "run", "node_val(head->next)", 0, "main.c", "node_val", - "CALLS", 2, "int node_val(const Node *n);", false, false, - "The visible prototype is a source declaration, but C extraction registers only function " - "definitions; no graph node or exact registry declaration exists for the prototype. Cross-TU " - "definition equivalence is a separate gap requiring an explicit linkage unit and compatible " - "effective linkage/signature.", - "Reviewed lang_specs.c c_func_types, extract_defs.c resolve_func_name_c_family, c_lsp.c " - "c_proof_collect/c_proof_domain; Source audit reported binding_not_found. Existing " - "resolver_c " - "source-prototype unit controls inject declaration identities and therefore do not test " - "materialization."}, - {"lrp_c_s6_base_method", "circle.c", "run", "shape_color(&c->base)", 0, "circle.c", - "shape_color", "CALLS", 3, "int shape_color(const Shape *s);", false, false, - "The visible prototype is a source declaration, but C extraction registers only function " - "definitions; no graph node or exact registry declaration exists for the prototype. Cross-TU " - "definition equivalence is a separate gap requiring an explicit linkage unit and compatible " - "effective linkage/signature.", - "Reviewed lang_specs.c c_func_types, extract_defs.c resolve_func_name_c_family, c_lsp.c " - "c_proof_collect/c_proof_domain; Source audit reported binding_not_found. Existing " - "resolver_c " - "source-prototype unit controls inject declaration identities and therefore do not test " - "materialization."}, - {"lrp_c_s7_generic_callback", "sort.c", "my_sort", "int_cmp(&arr[0], &arr[1])", 0, "sort.c", - "int_cmp", "CALLS", 2, "int int_cmp(const void *a, const void *b);", false, false, - "The visible prototype is a source declaration, but C extraction registers only function " - "definitions; no graph node or exact registry declaration exists for the prototype. Cross-TU " - "definition equivalence is a separate gap requiring an explicit linkage unit and compatible " - "effective linkage/signature.", - "Reviewed lang_specs.c c_func_types, extract_defs.c resolve_func_name_c_family, c_lsp.c " - "c_proof_collect/c_proof_domain; Source audit reported binding_not_found. Existing " - "resolver_c " - "source-prototype unit controls inject declaration identities and therefore do not test " - "materialization."}, - {"lrp_c_s8_field_call", "service.c", "run", "log_msg(&svc->logger, m)", 0, "service.c", - "log_msg", "CALLS", 3, "void log_msg(const Logger *l, const char *msg);", false, false, - "The visible prototype is a source declaration, but C extraction registers only function " - "definitions; no graph node or exact registry declaration exists for the prototype. Cross-TU " - "definition equivalence is a separate gap requiring an explicit linkage unit and compatible " - "effective linkage/signature.", - "Reviewed lang_specs.c c_func_types, extract_defs.c resolve_func_name_c_family, c_lsp.c " - "c_proof_collect/c_proof_domain; Source audit reported binding_not_found. Existing " - "resolver_c " - "source-prototype unit controls inject declaration identities and therefore do not test " - "materialization."}, + {"lrp_c_s5_chained_deref", "main.c", "run", "node_val(head->next)", 0, "node.c", "node_val", + "CALLS", 3, "int node_val(const Node *n) { return n->val; }", true, false, + "A prototype in the calling file declares the external name, and the one definition of " + "that name in the project is the target.", + "The earlier golden named the prototype, which has no graph node, and recorded a gap. The " + "translation-unit rules bind the definition, so the golden now names it."}, + {"lrp_c_s6_base_method", "circle.c", "run", "shape_color(&c->base)", 0, "shape.c", "shape_color", + "CALLS", 3, "int shape_color(const Shape *s) { return s->color; }", true, false, + "A prototype in the calling file declares the external name, and the one definition of " + "that name in the project is the target.", + "The earlier golden named the prototype, which has no graph node, and recorded a gap. The " + "translation-unit rules bind the definition, so the golden now names it."}, + {"lrp_c_s7_generic_callback", "sort.c", "my_sort", "int_cmp(&arr[0], &arr[1])", 0, "compare.c", + "int_cmp", "CALLS", 1, "int int_cmp(const void *a, const void *b) {", true, false, + "A prototype in the calling file declares the external name, and the one definition of " + "that name in the project is the target.", + "The earlier golden named the prototype, which has no graph node, and recorded a gap. The " + "translation-unit rules bind the definition, so the golden now names it."}, + {"lrp_c_s8_field_call", "service.c", "run", "log_msg(&svc->logger, m)", 0, "logger.c", "log_msg", + "CALLS", 3, "void log_msg(const Logger *l, const char *msg) { (void)l; (void)msg; }", true, false, + "A prototype in the calling file declares the external name, and the one definition of " + "that name in the project is the target.", + "The earlier golden named the prototype, which has no graph node, and recorded a gap. The " + "translation-unit rules bind the definition, so the golden now names it."}, {"lrp_c_s2_funcptr_in_struct", "main.c", "run", "o.compute(n)", 0, "ops.c", "double_it", "CALLS", 3, "static int double_it(int x) { return x * 2; }", false, false, "Requires proving make_ops linkage to this implementation, its returned struct field " diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index f9a5c1dc4..a608a889b 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -15550,8 +15550,8 @@ static void write_c_header_namesake_fixture(const char *tmp, int pad_files) { "{\n" "\tput_page(page);\n" "}\n"); - /* No include search roots configure this system spelling. A same-named - * project header is not evidence that the compiler would select it. */ + /* No include search roots are configured, and exactly one project file ends in + * this spelling, so it is the file any -I could select: the real kernel shape. */ write_temp_file(tmp, "drivers/net/unconfigured.c", "#include \n" "void unconfigured_free(struct page *page) { put_page(page); }\n"); @@ -15635,7 +15635,7 @@ TEST(pipeline_c_call_reaches_header_function_beside_field) { ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Field"), 0); ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Field"), 0); - ASSERT_EQ(labelled_call_count(s, project, "unconfigured_free", "put_page", "Function"), 0); + ASSERT_EQ(labelled_call_count(s, project, "unconfigured_free", "put_page", "Function"), 1); ASSERT_EQ(labelled_call_count(s, project, "unconfigured_free", "put_page", "Field"), 0); cbm_store_close(s); @@ -15673,7 +15673,7 @@ TEST(pipeline_c_call_reaches_header_function_beside_field_parallel) { ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Field"), 0); ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Field"), 0); - ASSERT_EQ(labelled_call_count(s, project, "unconfigured_free", "put_page", "Function"), 0); + ASSERT_EQ(labelled_call_count(s, project, "unconfigured_free", "put_page", "Function"), 1); ASSERT_EQ(labelled_call_count(s, project, "unconfigured_free", "put_page", "Field"), 0); cbm_store_close(s); @@ -16125,6 +16125,79 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con "conditional, parenthesized, return and binary expressions, not initializer_pair values; " "no guessed name supplies proof."; static const RQ_Expected expected[] = { + /* Parameter types and reads that the translation-unit rules now bind. */ + {.case_id = "c.field.use-beta-param-type", + .site = {.file = "use_beta.c", .caller = "use_beta.use_beta", .start_byte = 32, .end_byte = 36}, + .relation = "USAGE", + .target = {.file = "beta.h", + .suffix = "beta.Beta", + .label = "Class", + .declaration_line = 4, + .declaration_text = "typedef struct {"}}, + {.case_id = "c.field.guarded-alpha-read", + .site = {.file = "guarded.c", .caller = "guarded.guarded_alpha", .start_byte = 108, .end_byte = 113}, + .relation = "USAGE", + .target = {.file = "alpha.h", + .suffix = "alpha.Alpha.count", + .label = "Field", + .declaration_line = 5, + .declaration_text = " int count;"}}, + {.case_id = "c.field.guarded-beta-param-type", + .site = {.file = "guarded.c", .caller = "guarded.guarded_beta", .start_byte = 140, .end_byte = 144}, + .relation = "USAGE", + .target = {.file = "beta.h", + .suffix = "beta.Beta", + .label = "Class", + .declaration_line = 4, + .declaration_text = "typedef struct {"}}, + {.case_id = "c.field.guarded-beta-read", + .site = {.file = "guarded.c", .caller = "guarded.guarded_beta", .start_byte = 165, .end_byte = 170}, + .relation = "USAGE", + .target = {.file = "beta.h", + .suffix = "beta.Beta.count", + .label = "Field", + .declaration_line = 5, + .declaration_text = " int count;"}}, + {.case_id = "c.field.hdr-beta-param-type", + .site = {.file = "inline.h", .caller = "inline.hdr_beta", .start_byte = 168, .end_byte = 172}, + .relation = "USAGE", + .target = {.file = "beta.h", + .suffix = "beta.Beta", + .label = "Class", + .declaration_line = 4, + .declaration_text = "typedef struct {"}}, + {.case_id = "c.field.hdr-own-typedef-param-type", + .site = {.file = "inline.h", .caller = "inline.hdr_own_typedef", .start_byte = 408, .end_byte = 414}, + .relation = "USAGE", + .target = {.file = "inline.h", + .suffix = "inline.HdrBuf", + .label = "Class", + .declaration_line = 13, + .declaration_text = "typedef struct {"}}, + {.case_id = "c.field.use-remote-param-type", + .site = {.file = "mix.c", .caller = "mix.use_remote", .start_byte = 120, .end_byte = 124}, + .relation = "USAGE", + .target = {.file = "beta.h", + .suffix = "beta.Beta", + .label = "Class", + .declaration_line = 4, + .declaration_text = "typedef struct {"}}, + {.case_id = "c.field.global-alpha-read", + .site = {.file = "globals.c", .caller = "globals.read_alpha_global", .start_byte = 163, .end_byte = 168}, + .relation = "USAGE", + .target = {.file = "alpha.h", + .suffix = "alpha.Alpha.count", + .label = "Field", + .declaration_line = 5, + .declaration_text = " int count;"}}, + {.case_id = "c.field.global-beta-read", + .site = {.file = "globals.c", .caller = "globals.read_beta_global", .start_byte = 220, .end_byte = 225}, + .relation = "USAGE", + .target = {.file = "beta.h", + .suffix = "beta.Beta.count", + .label = "Field", + .declaration_line = 5, + .declaration_text = " int count;"}}, {.case_id = "c.field.alpha-count-0", /* Literal source: a->count */ .site = {.file = "use_alpha.c", @@ -16153,7 +16226,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 5, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-GUARDED-OWNER", .why = c_field_guard_why, .tried = c_field_guard_tried}, @@ -16169,7 +16242,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 5, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-WRITE-ROLE", .why = c_field_write_why, .tried = c_field_write_tried}, @@ -16201,7 +16274,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 5, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-ANONYMOUS-TYPEDEF-OWNER", .why = c_field_typedef_why, .tried = c_field_typedef_tried}, @@ -16217,7 +16290,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 5, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-WRITE-ROLE", .why = c_field_write_why, .tried = c_field_write_tried}, @@ -16233,7 +16306,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 6, .declaration_text = " int alpha_only;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-GUARDED-OWNER", .why = c_field_guard_why, .tried = c_field_guard_tried}, @@ -16249,7 +16322,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 6, .declaration_text = " int beta_only;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-ANONYMOUS-TYPEDEF-OWNER", .why = c_field_typedef_why, .tried = c_field_typedef_tried}, @@ -16308,7 +16381,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 10, .declaration_text = " struct Ops *ops;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-GUARDED-OWNER", .why = c_field_guard_why, .tried = c_field_guard_tried}, @@ -16372,7 +16445,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 14, .declaration_text = " struct Dev *devs, *spare;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-GUARDED-OWNER", .why = c_field_guard_why, .tried = c_field_guard_tried}, @@ -16388,7 +16461,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 11, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-COMPOSITE-RECEIVER", .why = c_field_chain_why, .tried = c_field_chain_tried}, @@ -16404,7 +16477,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 15, .declaration_text = " struct Dev fixed[2];"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-GUARDED-OWNER", .why = c_field_guard_why, .tried = c_field_guard_tried}, @@ -16420,7 +16493,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 11, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-COMPOSITE-RECEIVER", .why = c_field_chain_why, .tried = c_field_chain_tried}, @@ -16436,7 +16509,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 14, .declaration_text = " struct Dev *devs, *spare;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-GUARDED-OWNER", .why = c_field_guard_why, .tried = c_field_guard_tried}, @@ -16452,7 +16525,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 11, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-COMPOSITE-RECEIVER", .why = c_field_chain_why, .tried = c_field_chain_tried}, @@ -16466,7 +16539,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 5, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-GUARDED-OWNER", .why = c_field_guard_why, .tried = c_field_guard_tried}, @@ -16494,7 +16567,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 5, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-ANONYMOUS-TYPEDEF-OWNER", .why = c_field_typedef_why, .tried = c_field_typedef_tried}, @@ -16510,7 +16583,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 14, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-ANONYMOUS-TYPEDEF-OWNER", .why = c_field_typedef_why, .tried = c_field_typedef_tried}, @@ -16528,7 +16601,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 18, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-PP-HEADER-CERTIFICATE", .why = "The genuine field access needs the included header dependency closure. " "Expanded bytes, configuration and matching spans do not prove that " @@ -16547,7 +16620,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 5, .declaration_text = " int count;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-WRITE-ROLE", .why = c_field_write_why, .tried = c_field_write_tried}, @@ -16674,7 +16747,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Field", .declaration_line = 11, .declaration_text = " int id;"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-GUARDED-OWNER", .why = c_field_guard_why, .tried = c_field_guard_tried}, @@ -16775,7 +16848,7 @@ static bool c_member_fields_quality(cbm_store_t *store, const char *project, con .label = "Variable", .declaration_line = 6, .declaration_text = "static struct Ops table_ops = {"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-FIELD-INITIALIZER-VALUE", .why = c_field_global_value_why, .tried = c_field_global_value_tried}, @@ -27367,7 +27440,7 @@ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { .label = "Variable", .declaration_line = 1, .declaration_text = "enum Color { RED, GREEN };"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "C-ENUM-LEXICAL-VALUE", .why = "The enumerator has an exact declaration but no admitted enum-value lexical " "binding proof.", @@ -36420,7 +36493,7 @@ static bool cpp_recv_quality(const CppRecvRun *r, unsigned scenario) { .suffix = "SecdManager.SecdManager.getInstance", .declaration_line = 3, .declaration_text = "SecdManager *SecdManager::getInstance() {"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "CPP-FACTORY-VISIBILITY-PROOF", .why = "The unconfigured include roots and header-to-definition binding are not a closed " "declaration lookup; static class-member selection has no complete source proof " @@ -36469,7 +36542,7 @@ static bool cpp_recv_quality(const CppRecvRun *r, unsigned scenario) { .suffix = "SecdManager.Registry.instance", .declaration_line = 14, .declaration_text = "Registry &Registry::instance() {"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "CPP-FACTORY-VISIBILITY-PROOF", .why = "The unconfigured include roots and header-to-definition binding are not a closed " "declaration lookup; static class-member selection has no complete source proof " @@ -36515,7 +36588,7 @@ static bool cpp_recv_quality(const CppRecvRun *r, unsigned scenario) { .declaration_line = 21, .declaration_text = "std::unique_ptr Widget::create() { return " "std::make_unique(); }"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "CPP-FACTORY-VISIBILITY-PROOF", .why = "The unconfigured include roots and header-to-definition binding are not a closed " "declaration lookup; static class-member selection has no complete source proof " @@ -36562,7 +36635,7 @@ static bool cpp_recv_quality(const CppRecvRun *r, unsigned scenario) { .declaration_line = 25, .declaration_text = "std::shared_ptr Session::open() { return " "std::make_shared(); }"}, - .mode = RQ_KNOWN_GAP, + .mode = RQ_REQUIRED, .gap_id = "CPP-FACTORY-VISIBILITY-PROOF", .why = "The unconfigured include roots and header-to-definition binding are not a closed " "declaration lookup; static class-member selection has no complete source proof " diff --git a/tests/test_resolver_c.c b/tests/test_resolver_c.c index c2b2521de..05ec9dc61 100644 --- a/tests/test_resolver_c.c +++ b/tests/test_resolver_c.c @@ -29,8 +29,8 @@ static int c_proven_count(const CBMResolvedCallArray *rows, const char *target) int count = 0; for (int i = 0; i < rows->count; i++) { const CBMResolvedCall *row = &rows->items[i]; - if (row->kind != CBM_RESOLVED_FIELD_REFERENCE && c_row_admitted(row) && - (!target || !strcmp(row->callee_qn, target))) + if ((row->kind == CBM_RESOLVED_INVOCATION || row->kind == CBM_RESOLVED_CALL_REFERENCE) && + c_row_admitted(row) && (!target || !strcmp(row->callee_qn, target))) count++; } return count; @@ -304,7 +304,6 @@ TEST(resolver_c_quoted_header_fields_require_exact_source_owner) { const char *paths[] = {"alpha.h", "other.h", "beta.h"}; const char *modules[] = {"test.alpha", "test.other", "test.beta"}; int exact[7] = {0}, wrong[7] = {0}; - uint32_t flags[7] = {0}; for (int i = 0; i < 7; i++) { CBMArena arena; cbm_arena_init(&arena); @@ -340,18 +339,24 @@ TEST(resolver_c_quoted_header_fields_require_exact_source_owner) { .def_module_qn = "test.beta", .source_start_line = 1, .declaration_identity = "beta.h:Beta", - .field_defs = "count:int|beta_only:int"}}; - ASSERT( - cbm_c_collect_source_facts(&arena, header, (int)strlen(header), "test.alpha", defs, 1)); + .field_defs = "count:int|beta_only:int"}, + {.qualified_name = "test.alpha.@c-file-facts", + .short_name = "@c-file-facts", + .label = CBM_C_FILE_FACTS_LABEL, + .def_module_qn = "test.alpha"}}; + /* The header's facts carry its guard, which is how case 5 stays out. */ + CBMCFileFacts header_facts = {0}; + ASSERT(cbm_c_collect_file_facts(&arena, header, (int)strlen(header), "test.alpha", + CBM_LANG_CPP, defs, 1, &header_facts)); + defs[3].c_file_facts = &header_facts; ASSERT(cbm_c_collect_source_facts(&arena, other, (int)strlen(other), "test.other", defs + 1, 1)); ASSERT( cbm_c_collect_source_facts(&arena, beta, (int)strlen(beta), "test.beta", defs + 2, 1)); - flags[i] = defs[0].source_flags; CBMResolvedCallArray rows = {0}; int imports = i == 2 ? 2 : i == 3 ? 0 : 1; int first = i == 4 ? 2 : 0; - c_cross(&arena, source, false, defs, 3, paths + first, modules + first, imports, &rows); + c_cross(&arena, source, false, defs, 4, paths + first, modules + first, imports, &rows); const char *sites[] = {strstr(source, "a->count =") + 3, strstr(source, "a->count +") + 3, strstr(source, "a->alpha_only") + 3}; const char *targets[] = {"test.alpha.Alpha.count", "test.alpha.Alpha.count", @@ -359,7 +364,7 @@ TEST(resolver_c_quoted_header_fields_require_exact_source_owner) { unsigned seen = 0; for (int j = 0; j < rows.count; j++) { const CBMResolvedCall *row = &rows.items[j]; - if (!c_row_admitted(row)) + if (!c_row_admitted(row) || row->kind == CBM_RESOLVED_TYPE_REFERENCE) continue; int matched = -1; for (int k = 0; k < 3; k++) { @@ -389,20 +394,11 @@ TEST(resolver_c_quoted_header_fields_require_exact_source_owner) { for (int i = 2; i < 7; i++) ASSERT_EQ(exact[i], 0); ASSERT_EQ(exact[1], 3); - /* Quality gap C-GUARD-DECLARATION: all three guarded-header references - * genuinely target Alpha's fields. WHY: raw header text alone does not prove - * its declarations survived the caller's include-time macro environment. - * TRIED: the focused proof test resolves all three plain-header sites, but guarded source facts - * have flags=0. The five negative cases include a caller-suppressed guard and - * an inactive feature; simply descending through #if would make them wrong. - * Keep the independent three-site denominator and require promotion once - * configured/preprocessed declaration-presence proof recovers any site. */ - fprintf(stderr, - "[RQ-UNIT] C-GUARD-DECLARATION genuine=6 found=%d missing=%d wrong=0 " - "guarded_flags=%u plain_flags=%u\n", - exact[0] + exact[1], 6 - exact[0] - exact[1], flags[0], flags[1]); - if (exact[0] != 0) - FAIL("C-GUARD-DECLARATION recovered: promote the exact recovered sites to required"); + /* Every real C header is guarded, and every one of these three references + * targets Alpha's fields. The guard's one way to hide them, the caller defining + * it first, is case 5 above. An ordinary feature #ifdef around the struct, case + * 6, stays unknown. */ + ASSERT_EQ(exact[0], 3); PASS(); } @@ -472,7 +468,9 @@ TEST(resolver_cpp_direct_member_requires_source_class_and_visibility) { } ASSERT_EQ(proven[0], 1); ASSERT_EQ(proven[1], 0); - ASSERT_EQ(proven[2], 0); + /* `struct Engine : Other { void work() {} };`: a class's own member hides whatever + * its bases declare, so the unknown base changes nothing about this call. */ + ASSERT_EQ(proven[2], 1); ASSERT_EQ(proven[3], 0); PASS(); } @@ -642,8 +640,11 @@ TEST(resolver_c_external_prototype_requires_compatible_unique_definition) { {"int lib_value(void); int run(void) { return lib_value(); }", "int lib_value(void) { return 7; }", "#ifdef FEATURE\nint lib_value(void) { return 9; }\n#endif", false, 0}, + /* The only definition of this external name, written in a branch. A caller + * with a visible prototype either links to it or does not link to anything + * in this project, so this is the declaration the call selects. */ {"int lib_value(void); int run(void) { return lib_value(); }", - "#ifdef FEATURE\nint lib_value(void) { return 7; }\n#endif", NULL, false, 0}, + "#ifdef FEATURE\nint lib_value(void) { return 7; }\n#endif", NULL, false, 1}, {"int lib_value(); int run() { return lib_value(); }", "extern \"C\" { int lib_value() { return 7; } }", NULL, true, 0}, {"extern \"C\" { int lib_value(); } int run() { return lib_value(); }", @@ -993,8 +994,8 @@ TEST(resolver_c_unverified_preprocessing_cannot_persist_relationships) { * the first syntax error more than 16 levels under the root. The audit used to call * that a failure of the whole file, before the include table it reads later existed, * so the next declared binding dereferenced a NULL table and took the indexing worker - * down. The boundary is the error's own position: earlier sites keep their proofs, - * later ones do not, and declarations past it are simply not proven. */ + * down. The boundary is the error's own position within its function: the sites after + * it there are unread, and the next complete function has its own scopes. */ TEST(resolver_c_nested_syntax_error_bounds_proofs_at_the_error) { const char *source = "#include \"lib.h\"\n" "struct Holder { int value; };\n" @@ -1014,10 +1015,14 @@ TEST(resolver_c_nested_syntax_error_bounds_proofs_at_the_error) { "#ifdef EXTRA\n" " } else if (n == 10) { n = 11;\n" "#endif\n" - " } else { n = 12;\n" + " } else { n = 12; work();\n" " }\n" "}\n" "struct Holder shared_holder;\n" + "void declares(int n) {\n" + " n = ( int work ;\n" + " work();\n" + "}\n" "void after(void) { work(); }\n"; CBMLSPDef defs[] = {{.qualified_name = "test.main.work", .declaration_identity = "fixture:test.main.work", @@ -1035,9 +1040,13 @@ TEST(resolver_c_nested_syntax_error_bounds_proofs_at_the_error) { cbm_arena_init(&arena); CBMResolvedCallArray out = {0}; c_cross(&arena, source, false, defs, 2, paths, modules, 1, &out); - uint32_t error_region = (uint32_t)(strstr(source, "#ifdef EXTRA") - source); uint32_t before_call = (uint32_t)(strstr(source, "work(); }") - source); - int before_proofs = 0, proofs_past_error = 0; + uint32_t split_call = (uint32_t)(strstr(source, "n = 12; work();") - source + strlen("n = 12; ")); + uint32_t hidden_call = + (uint32_t)(strstr(source, "( int work ;\n work();") - source + strlen("( int work ;\n ")); + uint32_t after_call = (uint32_t)(strstr(source, "after(void) { work(); }") - source + + strlen("after(void) { ")); + int before_proofs = 0, split_proofs = 0, hidden_proofs = 0, after_proofs = 0, other = 0; for (int i = 0; i < out.count; i++) { const CBMResolvedCall *row = &out.items[i]; if (row->kind == CBM_RESOLVED_FIELD_REFERENCE || !c_row_admitted(row) || @@ -1045,12 +1054,1780 @@ TEST(resolver_c_nested_syntax_error_bounds_proofs_at_the_error) { continue; if (row->site_start_byte == before_call) before_proofs++; - if (row->site_start_byte >= error_region) - proofs_past_error++; + else if (row->site_start_byte == split_call) + split_proofs++; + else if (row->site_start_byte == hidden_call) + hidden_proofs++; + else if (row->site_start_byte == after_call) + after_proofs++; + else + other++; } cbm_arena_destroy(&arena); ASSERT_EQ(before_proofs, 1); - ASSERT_EQ(proofs_past_error, 0); + /* The #ifdef split error holds no type, so nothing it swallowed can hide `work`. */ + ASSERT_EQ(split_proofs, 1); + /* This one swallowed `int work`, a local that would hide the function. */ + ASSERT_EQ(hidden_proofs, 0); + ASSERT_EQ(after_proofs, 1); + ASSERT_EQ(other, 0); + PASS(); +} + +/* ── Translation-unit rules, on the shapes real C projects use ─────────────── + * Each fixture runs through both execution drivers. The sources are small copies + * of forms seen in redis and neovim, so a rule that only holds on tidy input + * fails here, and every positive sits beside the decoy that would falsify it. */ +typedef struct { + const char *suite; + const RQ_Source *sources; + size_t source_count; + const RQ_Expected *expected; + size_t expected_count; + const RQ_Forbidden *forbidden; + size_t forbidden_count; + const char *compile_commands; /* %s is the repository root */ + size_t known_gaps; /* rows in `expected` marked RQ_KNOWN_GAP */ +} CTUCase; + +static bool c_tu_run(const CTUCase *fixture, int driver) { + const char *root = th_mktempdir("cbm_c_tu"); + if (!root) + return false; + char repo[768], database[768]; + snprintf(repo, sizeof(repo), "%s/repo", root); + snprintf(database, sizeof(database), "%s/index.db", root); + bool ok = cbm_mkdir_p(repo, 0755); + for (size_t i = 0; ok && i < fixture->source_count; i++) + ok = th_write_file(TH_PATH(repo, fixture->sources[i].path), fixture->sources[i].text) == 0; + for (int i = 0; ok && i < 55; i++) { + char path[32]; + snprintf(path, sizeof(path), "filler%02d.c", i); + ok = th_write_file(TH_PATH(repo, path), "/* unrelated source */\n") == 0; + } + if (ok && fixture->compile_commands) { + char config[2048]; + snprintf(config, sizeof(config), fixture->compile_commands, repo); + ok = th_write_file(TH_PATH(repo, "compile_commands.json"), config) == 0; + } + const char *old_workers = getenv("CBM_WORKERS"); + char *saved_workers = old_workers ? strdup(old_workers) : NULL; + ok = ok && (!old_workers || saved_workers); + cbm_setenv("CBM_WORKERS", driver ? "4" : "1", 1); + cbm_pipeline_t *pipeline = ok ? cbm_pipeline_new(repo, database, CBM_MODE_FULL) : NULL; + ok = pipeline && cbm_pipeline_set_project_name(pipeline, "c-tu") && + cbm_pipeline_run(pipeline) == 0; + cbm_pipeline_free(pipeline); + if (saved_workers) { + cbm_setenv("CBM_WORKERS", saved_workers, 1); + free(saved_workers); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + cbm_store_t *store = ok ? cbm_store_open_path_existing(database) : NULL; + RQ_Report report = {0}; + ok = store && + rq_check(store, "c-tu", fixture->suite, fixture->sources, fixture->source_count, + fixture->expected, fixture->expected_count, fixture->forbidden, + fixture->forbidden_count, &report, stderr) && + ok; + if (store) + cbm_store_close(store); + th_cleanup(root); + ok = ok && report.genuine == fixture->expected_count && report.required_missing == 0 && + report.recovered_gaps == 0 && report.found + fixture->known_gaps == report.genuine && + report.missing == fixture->known_gaps && report.wrong + report.metadata_errors == 0; + if (!ok) + fprintf(stderr, "[C-TU] %s driver=%d genuine=%zu found=%zu missing=%zu wrong=%zu meta=%zu\n", + fixture->suite, driver, report.genuine, report.found, report.missing, report.wrong, + report.metadata_errors); + return ok; +} + +#define C_TU_EXPECT(id, file_, caller_, needle_, relation_, target_file, target_suffix, label_, \ + line_, text_) \ + {.case_id = id, \ + .site = {.file = file_, .caller = caller_, .needle = needle_}, \ + .relation = relation_, \ + .target = {.file = target_file, \ + .suffix = target_suffix, \ + .label = label_, \ + .declaration_line = line_, \ + .declaration_text = text_}} +#define C_TU_FORBID(id, file_, caller_, needle_, relation_) \ + {.case_id = id, \ + .site = {.file = file_, .caller = caller_, .needle = needle_}, \ + .relation = relation_, \ + .any_target = true} + +/* The byte of `word` after the first `context` in text: an identifier's own span. */ +static uint32_t c_tu_at(const char *text, const char *context, const char *word) { + const char *from = strstr(text, context); + const char *found = from ? strstr(from, word) : NULL; + return found ? (uint32_t)(found - text) : 0; +} +#define C_TU_WORD(file_, text_, caller_, context, word) \ + {.file = file_, \ + .caller = caller_, \ + .start_byte = c_tu_at(text_, context, word), \ + .end_byte = c_tu_at(text_, context, word) + (uint32_t)strlen(word)} + +/* redis: `server.h` includes `sds.h`, callers include only `server.h`, and each + * function is defined in a .c file no caller includes. */ +static const RQ_Source C_TU_REDIS[] = { + {.path = "src/sds.h", + .text = "#ifndef __SDS_H\n#define __SDS_H\n#include \n" + "typedef char *sds;\n" + "sds sdsnew(const char *init);\n" + "static inline int sdsavail(const sds s) { return s ? 1 : 0; }\n" + "#endif\n"}, + {.path = "src/sds.c", + .text = "#include \"sds.h\"\n" + "static sds alloc_it(const char *init) { return (sds)init; }\n" + "sds sdsnew(const char *init) { return alloc_it(init); }\n"}, + {.path = "src/server.h", + .text = "#ifndef __REDIS_H\n#define __REDIS_H\n#include \"sds.h\"\n" + "typedef struct client {\n int flags;\n sds querybuf;\n} client;\n" + "void addReply(client *c, int obj);\n" + "#endif\n"}, + {.path = "src/networking.c", + .text = "#include \"server.h\"\n" + "void addReply(client *c, int obj) { c->flags = obj; }\n"}, + {.path = "src/decoy.c", + .text = "static char *sdsnew(const char *init) { return (char *)init; }\n" + "char *decoy_use(void) { return sdsnew(\"x\"); }\n"}, + {.path = "src/t_string.c", + .text = "#include \"server.h\"\n" + "static int helper(client *c) { return c->flags; }\n" + "void setCommand(client *c) {\n" + " sds s = sdsnew(\"x\");\n" + " addReply(c, helper(c));\n" + " c->flags = sdsavail(s);\n" + "}\n"}}; + +TEST(resolver_c_header_prototype_binds_external_definition_through_includes) { + const RQ_Expected expected[] = { + C_TU_EXPECT("transitive-prototype", "src/t_string.c", "t_string.setCommand", + "sdsnew(\"x\")", "CALLS", "src/sds.c", "sds.sdsnew", "Function", 3, + "sds sdsnew(const char *init) { return alloc_it(init); }"), + C_TU_EXPECT("direct-prototype", "src/t_string.c", "t_string.setCommand", + "addReply(c, helper(c))", "CALLS", "src/networking.c", "networking.addReply", + "Function", 2, "void addReply(client *c, int obj) { c->flags = obj; }"), + C_TU_EXPECT("same-file-static", "src/t_string.c", "t_string.setCommand", "helper(c)", + "CALLS", "src/t_string.c", "t_string.helper", "Function", 2, + "static int helper(client *c) { return c->flags; }"), + C_TU_EXPECT("header-static-inline", "src/t_string.c", "t_string.setCommand", "sdsavail(s)", + "CALLS", "src/sds.h", "sds.sdsavail", "Function", 6, + "static inline int sdsavail(const sds s) { return s ? 1 : 0; }"), + C_TU_EXPECT("static-stays-in-its-file", "src/decoy.c", "decoy.decoy_use", "sdsnew(\"x\")", + "CALLS", "src/decoy.c", "decoy.sdsnew", "Function", 1, + "static char *sdsnew(const char *init) { return (char *)init; }"), + }; + const CTUCase fixture = {.suite = "c-tu-redis", + .sources = C_TU_REDIS, + .source_count = sizeof(C_TU_REDIS) / sizeof(C_TU_REDIS[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* Everything here looks like the positive above, and none of it is proved. */ +static const RQ_Source C_TU_ABSTAIN[] = { + /* Two programs in one repository, and neither copy includes the header. */ + {.path = "dup.h", .text = "int dup_value(int v);\n"}, + {.path = "one/dup_value.c", .text = "int dup_value(int v) { return v; }\n"}, + {.path = "two/dup_value.c", .text = "int dup_value(int v) { return v + 1; }\n"}, + {.path = "uses_dup.c", + .text = "#include \"dup.h\"\nint uses_dup(void) { return dup_value(1); }\n"}, + /* A macro of the name: which function runs depends on the expansion. */ + {.path = "wrapped.h", + .text = "#define wrapped(v) wrapped_impl(v)\nint wrapped(int v);\nint wrapped_impl(int v);\n"}, + {.path = "wrapped.c", .text = "int wrapped(int v) { return v; }\n"}, + {.path = "uses_wrapped.c", + .text = "#include \"wrapped.h\"\nint uses_wrapped(void) { return wrapped(1); }\n"}, + /* No include declares it: a name alone is not a binding. */ + {.path = "orphan.c", .text = "int orphan_value(int v) { return v; }\n"}, + {.path = "uses_orphan.c", .text = "int uses_orphan(void) { return orphan_value(1); }\n"}, + /* A parameter hides the function. */ + {.path = "shadow.h", .text = "int shadowed(int v);\n"}, + {.path = "shadow.c", .text = "int shadowed(int v) { return v; }\n"}, + {.path = "uses_shadow.c", + .text = "#include \"shadow.h\"\nint uses_shadow(int (*shadowed)(int)) { return shadowed(1); }\n"}, + /* The only definition is another file's static. */ + {.path = "hidden.h", .text = "int hidden_value(int v);\n"}, + {.path = "hidden.c", .text = "static int hidden_value(int v) { return v; }\n"}, + {.path = "uses_hidden.c", + .text = "#include \"hidden.h\"\nint uses_hidden(void) { return hidden_value(1); }\n"}, + /* A declaration whose parameters cannot match the definition's. */ + {.path = "arity.h", .text = "struct box;\nint arity_value(struct box *b);\n"}, + {.path = "arity.c", + .text = "struct box;\nint arity_value(struct box *b, int n) { return n; }\n"}, + {.path = "uses_arity.c", + .text = "#include \"arity.h\"\nint uses_arity(struct box *b) { return arity_value(b); }\n"}, + /* The include comes after the call. */ + {.path = "late.h", .text = "int late_value(int v);\n"}, + {.path = "late.c", .text = "int late_value(int v) { return v; }\n"}, + {.path = "uses_late.c", + .text = "int uses_late(void) { return late_value(1); }\n#include \"late.h\"\n"}, + /* A caller that defines the guard first keeps the header's declarations out. */ + {.path = "guarded.h", + .text = "#ifndef GUARDED_H\n#define GUARDED_H\nint guarded_value(int v);\n#endif\n"}, + {.path = "guarded.c", .text = "int guarded_value(int v) { return v; }\n"}, + {.path = "uses_guarded.c", + .text = "#define GUARDED_H\n#include \"guarded.h\"\n" + "int uses_guarded(void) { return guarded_value(1); }\n"}}; + +TEST(resolver_c_lookalike_calls_abstain) { + const RQ_Forbidden forbidden[] = { + C_TU_FORBID("two-definitions", "uses_dup.c", "uses_dup.uses_dup", "dup_value(1)", "CALLS"), + C_TU_FORBID("macro-of-the-name", "uses_wrapped.c", "uses_wrapped.uses_wrapped", + "wrapped(1)", "CALLS"), + C_TU_FORBID("not-declared", "uses_orphan.c", "uses_orphan.uses_orphan", "orphan_value(1)", + "CALLS"), + C_TU_FORBID("parameter-shadow", "uses_shadow.c", "uses_shadow.uses_shadow", "shadowed(1)", + "CALLS"), + C_TU_FORBID("other-files-static", "uses_hidden.c", "uses_hidden.uses_hidden", + "hidden_value(1)", "CALLS"), + C_TU_FORBID("arity-mismatch", "uses_arity.c", "uses_arity.uses_arity", "arity_value(b)", + "CALLS"), + C_TU_FORBID("include-after-call", "uses_late.c", "uses_late.uses_late", "late_value(1)", + "CALLS"), + C_TU_FORBID("guard-defined-first", "uses_guarded.c", "uses_guarded.uses_guarded", + "guarded_value(1)", "CALLS"), + }; + const CTUCase fixture = {.suite = "c-tu-abstain", + .sources = C_TU_ABSTAIN, + .source_count = sizeof(C_TU_ABSTAIN) / sizeof(C_TU_ABSTAIN[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* Several definitions of one name in one repository: the one whose own file + * includes this header was compiled against this declaration. */ +static const RQ_Source C_TU_COPIES[] = { + {.path = "app/sds.h", .text = "char *sdsdup(const char *s);\n"}, + {.path = "app/sds.c", + .text = "#include \"sds.h\"\nchar *sdsdup(const char *s) { return (char *)s; }\n"}, + {.path = "copy/sds.h", .text = "char *sdsdup(const char *s);\n"}, + {.path = "copy/sds.c", + .text = "#include \"sds.h\"\nchar *sdsdup(const char *s) { return (char *)s + 1; }\n"}, + {.path = "app/main.c", + .text = "#include \"sds.h\"\nchar *app_copy(void) { return sdsdup(\"a\"); }\n"}, + {.path = "copy/use.c", + .text = "#include \"sds.h\"\nchar *other_copy(void) { return sdsdup(\"v\"); }\n"}}; + +TEST(resolver_c_copied_library_binds_the_copy_compiled_against_its_header) { + const RQ_Expected expected[] = { + C_TU_EXPECT("app-copy", "app/main.c", "main.app_copy", "sdsdup(\"a\")", "CALLS", + "app/sds.c", "app.sds.sdsdup", "Function", 2, + "char *sdsdup(const char *s) { return (char *)s; }"), + C_TU_EXPECT("other-copy", "copy/use.c", "use.other_copy", "sdsdup(\"v\")", "CALLS", + "copy/sds.c", "copy.sds.sdsdup", "Function", 2, + "char *sdsdup(const char *s) { return (char *)s + 1; }"), + }; + const CTUCase fixture = {.suite = "c-tu-copies", + .sources = C_TU_COPIES, + .source_count = sizeof(C_TU_COPIES) / sizeof(C_TU_COPIES[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* The kernel shape behind the put_page audit: `-I include` selects one of two + * headers with the same spelling. Without the flag this spelling is a collision. */ +static const RQ_Source C_TU_KERNEL[] = { + {.path = "include/linux/mm.h", + .text = "struct page;\nstatic inline void put_page(struct page *page) { (void)page; }\n"}, + {.path = "tools/include/linux/mm.h", + .text = "struct page;\nstatic inline void put_page(struct page *page) { (void)page; }\n"}, + {.path = "arch/arm64/include/asm/kvm_pgtable.h", + .text = "struct kvm_pgtable_mm_ops {\n\tvoid (*put_page)(void *addr);\n};\n"}, + {.path = "drivers/net/rx.c", + .text = "#include \nvoid recv_free(struct page *page) { put_page(page); }\n"}, + {.path = "drivers/net/unconfigured.c", + .text = "#include \nvoid other_free(struct page *page) { put_page(page); }\n"}}; + +TEST(resolver_c_configured_include_root_reaches_calls) { + const RQ_Expected expected[] = { + C_TU_EXPECT("configured-root", "drivers/net/rx.c", "rx.recv_free", "put_page(page)", + "CALLS", "include/linux/mm.h", "include.linux.mm.put_page", "Function", 2, + "static inline void put_page(struct page *page) { (void)page; }"), + }; + const RQ_Forbidden forbidden[] = { + C_TU_FORBID("no-root-two-headers", "drivers/net/unconfigured.c", "unconfigured.other_free", + "put_page(page)", "CALLS"), + }; + const CTUCase fixture = { + .suite = "c-tu-kernel", + .sources = C_TU_KERNEL, + .source_count = sizeof(C_TU_KERNEL) / sizeof(C_TU_KERNEL[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0]), + .compile_commands = "[{\"directory\":\"%s\",\"file\":\"drivers/net/rx.c\"," + "\"command\":\"cc -I include -c drivers/net/rx.c\"}]"}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* The same fixture with no build configuration: the angle spelling names one + * project file, so it is the file any -I would select. */ +TEST(resolver_c_unique_include_spelling_reaches_calls_without_configuration) { + const RQ_Source sources[] = { + {.path = "include/linux/mm.h", + .text = "struct page;\nstatic inline void put_page(struct page *page) { (void)page; }\n"}, + {.path = "arch/arm64/include/asm/kvm_pgtable.h", + .text = "struct kvm_pgtable_mm_ops {\n\tvoid (*put_page)(void *addr);\n};\n"}, + {.path = "drivers/net/rx.c", + .text = "#include \nvoid recv_free(struct page *page) { put_page(page); }\n"}}; + const RQ_Expected expected[] = { + C_TU_EXPECT("unique-spelling", "drivers/net/rx.c", "rx.recv_free", "put_page(page)", + "CALLS", "include/linux/mm.h", "include.linux.mm.put_page", "Function", 2, + "static inline void put_page(struct page *page) { (void)page; }"), + }; + const CTUCase fixture = {.suite = "c-tu-unique-spelling", + .sources = sources, + .source_count = sizeof(sources) / sizeof(sources[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* neovim: headers are spelled from a root under src/, and `foo.h` declares what + * `foo.c` exports through `foo.h.generated.h`, which the build writes. */ +static const RQ_Source C_TU_NEOVIM[] = { + {.path = "src/nvim/api/buffer.h", + .text = "#pragma once\n#include \"api/buffer.h.generated.h\"\n"}, + {.path = "src/nvim/api/buffer.c", + .text = "#include \"nvim/api/buffer.h\"\n" + "#include \"api/buffer.c.generated.h\"\n" + "int nvim_buf_line_count(int buf) { return count_lines(buf); }\n" + "static int count_lines(int buf) { return buf; }\n" + "static int private_helper(int buf) { return buf; }\n"}, + {.path = "src/nvim/window.c", + .text = "#include \"nvim/api/buffer.h\"\n" + "int win_lines(int buf) { return nvim_buf_line_count(buf); }\n" + "int win_private(int buf) { return private_helper(buf); }\n"}}; + +TEST(resolver_c_generated_declaration_header_exposes_its_source) { + const RQ_Expected expected[] = { + C_TU_EXPECT("generated-header", "src/nvim/window.c", "window.win_lines", + "nvim_buf_line_count(buf)", "CALLS", "src/nvim/api/buffer.c", + "api.buffer.nvim_buf_line_count", "Function", 3, + "int nvim_buf_line_count(int buf) { return count_lines(buf); }"), + C_TU_EXPECT("own-definition-declared-by-generated-header", "src/nvim/api/buffer.c", + "buffer.nvim_buf_line_count", "count_lines(buf)", "CALLS", + "src/nvim/api/buffer.c", "api.buffer.count_lines", "Function", 4, + "static int count_lines(int buf) { return buf; }"), + }; + const RQ_Forbidden forbidden[] = { + C_TU_FORBID("generated-header-holds-no-statics", "src/nvim/window.c", "window.win_private", + "private_helper(buf)", "CALLS"), + }; + const CTUCase fixture = {.suite = "c-tu-neovim", + .sources = C_TU_NEOVIM, + .source_count = sizeof(C_TU_NEOVIM) / sizeof(C_TU_NEOVIM[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* Member access through typedefs, chains and extern objects, from redis. */ +#define C_TU_FIELDS_USE \ + "#include \"server.h\"\n" \ + "int read_flags(client *c) { return c->flags; }\n" \ + "int read_chain(client *c) { return c->db->id; }\n" \ + "int read_alias(client_ptr c) { return c->flags; }\n" \ + "int read_global(void) { return server.port; }\n" \ + "int read_deep(void) { return server.current->db->id; }\n" \ + "client *get_client(void);\n" \ + "int read_returned(void) { return get_client()->flags; }\n" \ + "void write_flags(client *c) { c->flags = 1; }\n" \ + "void write_global(void) { server.port = 7; }\n" \ + "void bump_flags(client *c) { c->flags++; }\n" \ + "int local_shadow(void) { struct { int port; } server; server.port = 1; return server.port; }\n" +static const RQ_Source C_TU_FIELDS[] = { + {.path = "src/server.h", + .text = "#ifndef SERVER_H\n#define SERVER_H\n" + "typedef struct redisDb {\n int id;\n} redisDb;\n" + "typedef struct client {\n int flags;\n redisDb *db;\n" + "#ifdef USE_TLS\n int tls;\n#endif\n} client;\n" + "typedef struct client *client_ptr;\n" + "struct redisServer {\n int port;\n client *current;\n};\n" + "extern struct redisServer server;\n" + "#endif\n"}, + {.path = "src/server.c", + .text = "#include \"server.h\"\nstruct redisServer server;\n" + "client *get_client(void) { return server.current; }\n"}, + {.path = "src/other.h", .text = "struct other {\n int flags;\n};\n"}, + {.path = "src/use.c", .text = C_TU_FIELDS_USE}}; + +TEST(resolver_c_member_access_follows_written_declarations) { + const char *use = C_TU_FIELDS_USE; + const RQ_Target flags = {.file = "src/server.h", + .suffix = "client.flags", + .label = "Field", + .declaration_line = 7, + .declaration_text = " int flags;"}; + const RQ_Target id = {.file = "src/server.h", + .suffix = "redisDb.id", + .label = "Field", + .declaration_line = 4, + .declaration_text = " int id;"}; + const RQ_Target db = {.file = "src/server.h", + .suffix = "client.db", + .label = "Field", + .declaration_line = 8, + .declaration_text = " redisDb *db;"}; + const RQ_Target port = {.file = "src/server.h", + .suffix = "redisServer.port", + .label = "Field", + .declaration_line = 15, + .declaration_text = " int port;"}; + const RQ_Target current = {.file = "src/server.h", + .suffix = "redisServer.current", + .label = "Field", + .declaration_line = 16, + .declaration_text = " client *current;"}; + const RQ_Target client_type = {.file = "src/server.h", + .suffix = "server.client", + .label = "Class", + .declaration_line = 6, + .declaration_text = "typedef struct client {"}; + const RQ_Target client_ptr = {.file = "src/server.h", + .suffix = "server.client_ptr", + .label = "Type", + .declaration_line = 13, + .declaration_text = "typedef struct client *client_ptr;"}; + const RQ_Target global = {.file = "src/server.c", + .suffix = "server.server", + .label = "Variable", + .declaration_line = 2, + .declaration_text = "struct redisServer server;"}; + const RQ_Expected expected[] = { + {.case_id = "signature-type", + .site = C_TU_WORD("src/use.c", use, "use.read_flags", "read_flags(", "client"), + .relation = "USAGE", + .target = client_type}, + {.case_id = "typedef-read", + .site = C_TU_WORD("src/use.c", use, "use.read_flags", "return c->flags", "flags"), + .relation = "USAGE", + .target = flags}, + {.case_id = "chain-type", + .site = C_TU_WORD("src/use.c", use, "use.read_chain", "read_chain(", "client"), + .relation = "USAGE", + .target = client_type}, + {.case_id = "chain-read-first", + .site = C_TU_WORD("src/use.c", use, "use.read_chain", "c->db->id", "db"), + .relation = "USAGE", + .target = db}, + {.case_id = "chain-read-second", + .site = C_TU_WORD("src/use.c", use, "use.read_chain", "c->db->id", "id"), + .relation = "USAGE", + .target = id}, + {.case_id = "pointer-typedef-type", + .site = C_TU_WORD("src/use.c", use, "use.read_alias", "read_alias(", "client_ptr"), + .relation = "USAGE", + .target = client_ptr}, + {.case_id = "pointer-typedef-read", + .site = C_TU_WORD("src/use.c", use, "use.read_alias", "client_ptr c) { return c->", "flags"), + .relation = "USAGE", + .target = flags}, + {.case_id = "extern-object", + .site = C_TU_WORD("src/use.c", use, "use.read_global", "return server.port", "server"), + .relation = "USAGE", + .target = global}, + {.case_id = "extern-object-read", + .site = C_TU_WORD("src/use.c", use, "use.read_global", "return server.port", "port"), + .relation = "USAGE", + .target = port}, + {.case_id = "extern-chain-object", + .site = C_TU_WORD("src/use.c", use, "use.read_deep", "server.current->db->id", "server"), + .relation = "USAGE", + .target = global}, + {.case_id = "extern-chain-first", + .site = C_TU_WORD("src/use.c", use, "use.read_deep", "server.current->db->id", "current"), + .relation = "USAGE", + .target = current}, + {.case_id = "extern-chain-second", + .site = C_TU_WORD("src/use.c", use, "use.read_deep", "server.current->db->id", "db"), + .relation = "USAGE", + .target = db}, + {.case_id = "extern-chain-third", + .site = C_TU_WORD("src/use.c", use, "use.read_deep", "server.current->db->id", "id"), + .relation = "USAGE", + .target = id}, + {.case_id = "returned-struct-read", + .site = C_TU_WORD("src/use.c", use, "use.read_returned", "get_client()->flags", "flags"), + .relation = "USAGE", + .target = flags}, + {.case_id = "assignment-writes", + .site = C_TU_WORD("src/use.c", use, "use.write_flags", "c->flags = 1", "flags"), + .relation = "WRITES", + .target = flags}, + {.case_id = "extern-assignment-writes", + .site = C_TU_WORD("src/use.c", use, "use.write_global", "server.port = 7", "port"), + .relation = "WRITES", + .target = port}, + {.case_id = "increment-writes", + .site = C_TU_WORD("src/use.c", use, "use.bump_flags", "c->flags++", "flags"), + .relation = "WRITES", + .target = flags}, + {.case_id = "increment-parameter-type", + .site = C_TU_WORD("src/use.c", use, "use.bump_flags", "bump_flags(client", "client"), + .relation = "USAGE", + .target = client_type}, + {.case_id = "increment-also-reads", + .site = C_TU_WORD("src/use.c", use, "use.bump_flags", "c->flags++", "flags"), + .relation = "USAGE", + .target = flags}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "block-scope-struct-hides-the-global", + .site = C_TU_WORD("src/use.c", use, "use.local_shadow", "server.port = 1", "port"), + .relation = "WRITES", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-fields", + .sources = C_TU_FIELDS, + .source_count = sizeof(C_TU_FIELDS) / sizeof(C_TU_FIELDS[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* Bare value and type names: a global through its extern declaration, an + * enumerator from a header, a written type, each beside what would hide it. */ +#define C_TU_VALUES_USE \ + "#include \"server.h\"\n" \ + "int read_global(void) { return hz; }\n" \ + "int read_enum(void) { return BLOCKED_LIST; }\n" \ + "mstime_t read_type(mstime_t when) { mstime_t next = when; return next; }\n" \ + "void write_global(void) { hz = 3; }\n" \ + "int param_shadow(int hz) { return hz; }\n" \ + "int local_shadow(void) { int hz = 1; return hz; }\n" \ + "int undeclared(void) { return not_declared_anywhere; }\n" \ + "int local_enum(void) { enum { BLOCKED_LIST = 9 }; return BLOCKED_LIST; }\n" \ + "int local_tag(void) { struct mstime_t { int v; } m; struct mstime_t *p = &m; return p->v; }\n" +static const RQ_Source C_TU_VALUES[] = { + {.path = "src/server.h", + .text = "#ifndef SERVER_H\n#define SERVER_H\n" + "enum blocked_type {\n BLOCKED_NONE,\n BLOCKED_LIST\n};\n" + "typedef int mstime_t;\n" + "extern int hz;\n" + "#endif\n"}, + {.path = "src/server.c", .text = "#include \"server.h\"\nint hz;\n"}, + {.path = "src/elsewhere.c", .text = "static int hz;\nint read_static(void) { return hz; }\n"}, + {.path = "src/use.c", .text = C_TU_VALUES_USE}}; + +TEST(resolver_c_bare_values_and_types_bind_in_the_translation_unit) { + const char *use = C_TU_VALUES_USE; + const char *elsewhere = C_TU_VALUES[2].text; + const RQ_Target mstime = {.file = "src/server.h", + .suffix = "server.mstime_t", + .label = "Type", + .declaration_line = 7, + .declaration_text = "typedef int mstime_t;"}; + const RQ_Target hz = {.file = "src/server.c", + .suffix = "server.hz", + .label = "Variable", + .declaration_line = 2, + .declaration_text = "int hz;"}; + const RQ_Target blocked = {.file = "src/server.h", + .suffix = "server.BLOCKED_LIST", + .label = "Variable", + .declaration_line = 5, + .declaration_text = " BLOCKED_LIST"}; + const RQ_Target own_static = {.file = "src/elsewhere.c", + .suffix = "elsewhere.hz", + .label = "Variable", + .declaration_line = 1, + .declaration_text = "static int hz;"}; + const RQ_Expected expected[] = { + {.case_id = "extern-object", + .site = C_TU_WORD("src/use.c", use, "use.read_global", "return hz", "hz"), + .relation = "USAGE", + .target = hz}, + {.case_id = "header-enumerator", + .site = C_TU_WORD("src/use.c", use, "use.read_enum", "return BLOCKED_LIST", "BLOCKED_LIST"), + .relation = "USAGE", + .target = blocked}, + {.case_id = "extern-object-write", + .site = C_TU_WORD("src/use.c", use, "use.write_global", "hz = 3", "hz"), + .relation = "WRITES", + .target = hz}, + {.case_id = "own-file-static", + .site = C_TU_WORD("src/elsewhere.c", elsewhere, "elsewhere.read_static", "return hz", "hz"), + .relation = "USAGE", + .target = own_static}, + {.case_id = "signature-return-type", + .site = C_TU_WORD("src/use.c", use, "use.read_type", "mstime_t read_type", "mstime_t"), + .relation = "USAGE", + .target = mstime}, + {.case_id = "signature-parameter-type", + .site = C_TU_WORD("src/use.c", use, "use.read_type", "mstime_t when", "mstime_t"), + .relation = "USAGE", + .target = mstime}, + {.case_id = "local-declaration-type", + .site = C_TU_WORD("src/use.c", use, "use.read_type", "mstime_t next", "mstime_t"), + .relation = "USAGE", + .target = mstime}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "parameter-hides-global", + .site = C_TU_WORD("src/use.c", use, "use.param_shadow", "return hz", "hz"), + .relation = "USAGE", + .any_target = true}, + {.case_id = "block-scope-hides-global", + .site = C_TU_WORD("src/use.c", use, "use.local_shadow", "int hz = 1; return hz", "return hz"), + .relation = "USAGE", + .any_target = true}, + {.case_id = "no-declaration", + .site = C_TU_WORD("src/use.c", use, "use.undeclared", "return not_", "not_declared_anywhere"), + .relation = "USAGE", + .any_target = true}, + {.case_id = "block-enumerator-hides-header", + .site = C_TU_WORD("src/use.c", use, "use.local_enum", "return BLOCKED_LIST", "BLOCKED_LIST"), + .relation = "USAGE", + .any_target = true}, + {.case_id = "block-tag-hides-header-type", + .site = C_TU_WORD("src/use.c", use, "use.local_tag", "struct mstime_t *p", "mstime_t"), + .relation = "USAGE", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-values", + .sources = C_TU_VALUES, + .source_count = sizeof(C_TU_VALUES) / sizeof(C_TU_VALUES[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* C++ as LLVM writes it: namespaces, a class in a header with its members defined in + * a source file no caller includes, base classes, implicit members. */ +#define C_TU_CXX_HEADER \ + "#ifndef LLVM_ADT_STRINGREF_H\n" \ + "#define LLVM_ADT_STRINGREF_H\n" \ + "namespace llvm {\n" \ + "class StringRef {\n" \ + "public:\n" \ + " int size() const { return Length; }\n" \ + " bool empty() const;\n" \ + " int Count;\n" \ + "private:\n" \ + " int secret() const { return 1; }\n" \ + " int Length;\n" \ + "};\n" \ + "class Derived : public StringRef {\n" \ + "public:\n" \ + " int own() { return size(); }\n" \ + "};\n" \ + "int getAsInteger(StringRef S);\n" \ + "inline int headerHelper(StringRef S) { return S.empty() ? 0 : 1; }\n" \ + "struct Arrow {\n" \ + " StringRef *operator->();\n" \ + " int Count;\n" \ + "};\n" \ + "}\n" \ + "#endif\n" +#define C_TU_CXX_IMPL \ + "#include \"llvm/ADT/StringRef.h\"\n" \ + "using namespace llvm;\n" \ + "bool StringRef::empty() const { return Length == 0; }\n" \ + "int llvm::getAsInteger(StringRef S) { return S.size(); }\n" +#define C_TU_CXX_USE \ + "#include \"llvm/ADT/StringRef.h\"\n" \ + "namespace llvm {\n" \ + "int useInline(StringRef S) { return S.size(); }\n" \ + "int useOutOfLine(StringRef S) { return S.empty() ? 0 : 1; }\n" \ + "int useFree(StringRef S) { return getAsInteger(S); }\n" \ + "int useHeader(StringRef S) { return headerHelper(S); }\n" \ + "int usePointer(StringRef *S) { return S->Count; }\n" \ + "int useBase(Derived D) { return D.size(); }\n" \ + "int usePrivate(StringRef S) { return S.secret(); }\n" \ + "int useArrow(Arrow A) { return A->Count; }\n" \ + "int useDot(Arrow A) { return A.Count; }\n" \ + "}\n" \ + "int outer(llvm::StringRef S) { return llvm::getAsInteger(S); }\n" +#define C_TU_CXX_ADL \ + "namespace a {\n" \ + "struct W {};\n" \ + "int pick(W w);\n" \ + "}\n" \ + "namespace b {\n" \ + "using namespace a;\n" \ + "int pick(W w);\n" \ + "int run(W w) { return pick(w); }\n" \ + "}\n" +static const RQ_Source C_TU_CXX[] = {{.path = "llvm/include/llvm/ADT/StringRef.h", + .text = C_TU_CXX_HEADER}, + {.path = "llvm/lib/Support/StringRef.cpp", + .text = C_TU_CXX_IMPL}, + {.path = "llvm/lib/Support/Use.cpp", .text = C_TU_CXX_USE}, + {.path = "adl.cpp", .text = C_TU_CXX_ADL}}; + +TEST(resolver_cpp_members_and_namespaces_bind_through_the_translation_unit) { + const char *header = "llvm/include/llvm/ADT/StringRef.h"; + const char *impl = "llvm/lib/Support/StringRef.cpp"; + const char *use_file = "llvm/lib/Support/Use.cpp"; + const char *use = C_TU_CXX_USE; + const char *impl_text = C_TU_CXX_IMPL; + const RQ_Target size = {.file = header, + .suffix = "llvm.StringRef.size", + .label = "Method", + .declaration_line = 6, + .declaration_text = " int size() const { return Length; }"}; + const RQ_Target empty = {.file = impl, + .suffix = "StringRef.StringRef.empty", + .label = "Method", + .declaration_line = 3, + .declaration_text = + "bool StringRef::empty() const { return Length == 0; }"}; + const RQ_Target free_function = {.file = impl, + .suffix = "llvm.getAsInteger", + .label = "Method", + .declaration_line = 4, + .declaration_text = + "int llvm::getAsInteger(StringRef S) { return S.size(); }"}; + const RQ_Target inline_function = { + .file = header, + .suffix = "llvm.headerHelper", + .label = "Function", + .declaration_line = 18, + .declaration_text = "inline int headerHelper(StringRef S) { return S.empty() ? 0 : 1; }"}; + const RQ_Target count = {.file = header, + .suffix = "llvm.StringRef.Count", + .label = "Field", + .declaration_line = 8, + .declaration_text = " int Count;"}; + const RQ_Target arrow_count = {.file = header, + .suffix = "llvm.Arrow.Count", + .label = "Field", + .declaration_line = 21, + .declaration_text = " int Count;"}; + const RQ_Target length = {.file = header, + .suffix = "llvm.StringRef.Length", + .label = "Field", + .declaration_line = 11, + .declaration_text = " int Length;"}; + const RQ_Target string_ref = {.file = header, + .suffix = "llvm.StringRef", + .label = "Class", + .declaration_line = 4, + .declaration_text = "class StringRef {"}; + const RQ_Target arrow_type = {.file = header, + .suffix = "llvm.Arrow", + .label = "Class", + .declaration_line = 19, + .declaration_text = "struct Arrow {"}; + const RQ_Expected expected[] = { + {.case_id = "inline-member", + .site = {.file = use_file, .caller = "llvm.useInline", .needle = "S.size()"}, + .relation = "CALLS", + .target = size}, + {.case_id = "out-of-line-member", + .site = {.file = use_file, .caller = "llvm.useOutOfLine", .needle = "S.empty()"}, + .relation = "CALLS", + .target = empty}, + {.case_id = "namespace-function-qualified-definition", + .site = {.file = use_file, .caller = "llvm.useFree", .needle = "getAsInteger(S)"}, + .relation = "CALLS", + .target = free_function}, + {.case_id = "header-inline-function", + .site = {.file = use_file, .caller = "llvm.useHeader", .needle = "headerHelper(S)"}, + .relation = "CALLS", + .target = inline_function}, + {.case_id = "base-class-member", + .site = {.file = use_file, .caller = "llvm.useBase", .needle = "D.size()"}, + .relation = "CALLS", + .target = size}, + {.case_id = "qualified-call", + .site = {.file = use_file, .caller = "Use.outer", .needle = "llvm::getAsInteger(S)"}, + .relation = "CALLS", + .target = free_function}, + {.case_id = "member-call-in-out-of-line-definition", + .site = {.file = impl, .caller = "llvm.getAsInteger", .needle = "S.size()"}, + .relation = "CALLS", + .target = size}, + {.case_id = "overloaded-arrow-parameter-type", + .site = C_TU_WORD(use_file, use, "llvm.useArrow", "(Arrow A) { return A->", "Arrow"), + .relation = "USAGE", + .target = arrow_type}, + {.case_id = "pointer-member-type", + .site = C_TU_WORD(use_file, use, "llvm.usePointer", "usePointer(", "StringRef"), + .relation = "USAGE", + .target = string_ref}, + {.case_id = "pointer-member", + .site = C_TU_WORD(use_file, use, "llvm.usePointer", "S->Count", "Count"), + .relation = "USAGE", + .target = count}, + {.case_id = "dot-on-a-class-with-arrow-type", + .site = C_TU_WORD(use_file, use, "llvm.useDot", "useDot(", "Arrow"), + .relation = "USAGE", + .target = arrow_type}, + {.case_id = "dot-on-a-class-with-arrow", + .site = C_TU_WORD(use_file, use, "llvm.useDot", "A.Count", "Count"), + .relation = "USAGE", + .target = arrow_count}, + {.case_id = "implicit-member-in-out-of-line-definition", + .site = C_TU_WORD(impl, impl_text, "StringRef.StringRef.empty", "return Length", "Length"), + .relation = "USAGE", + .target = length}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "private-member-from-outside", + .site = {.file = use_file, .caller = "llvm.usePrivate", .needle = "S.secret()"}, + .relation = "CALLS", + .any_target = true}, + {.case_id = "overloaded-arrow-is-not-a-field-of-this-class", + .site = C_TU_WORD(use_file, use, "llvm.useArrow", "A->Count", "Count"), + .relation = "USAGE", + .any_target = true}, + {.case_id = "argument-dependent-lookup-adds-a-candidate", + .site = {.file = "adl.cpp", .caller = "b.run", .needle = "pick(w)"}, + .relation = "CALLS", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-cxx", + .sources = C_TU_CXX, + .source_count = sizeof(C_TU_CXX) / sizeof(C_TU_CXX[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* What the C rules leave unbound on real code. Each row is a genuine reference that the + * resolver abstains on today; an edge here would be promoted to required, and an edge to + * anything else is reported as wrong. */ +#define C_TU_GAPS_HEADER \ + "#ifndef GAPS_H\n" \ + "#define GAPS_H\n" \ + "typedef struct tabpage_S {\n" \ + " struct tabpage_S *tp_next;\n" \ + "} tabpage_T;\n" \ + "extern tabpage_T *first_tabpage;\n" \ + "#define FOR_ALL_TABS(tp) for (tabpage_T *(tp) = first_tabpage; (tp); (tp) = (tp)->tp_next)\n" \ + "#define LOG_LINE(s) log_line(s)\n" \ + "void log_line(const char *s);\n" \ + "struct item {\n" \ + " union {\n" \ + " int value;\n" \ + " };\n" \ + "};\n" \ + "#endif\n" +#define C_TU_GAPS_SOURCE \ + "#include \"gaps.h\"\n" \ + "tabpage_T *first_tabpage;\n" \ + "void log_line(const char *s) { (void)s; }\n" +#define C_TU_GAPS_USE \ + "#include \"gaps.h\"\n" \ + "int use_loop(void) {\n" \ + " int n = 0;\n" \ + " FOR_ALL_TABS(tp) {\n" \ + " n += tp->tp_next != 0;\n" \ + " }\n" \ + " return n;\n" \ + "}\n" \ + "void use_macro(void) { LOG_LINE(\"x\"); }\n" \ + "int use_union(struct item *o) { return o->value; }\n" +static const RQ_Source C_TU_GAPS[] = {{.path = "gaps.h", .text = C_TU_GAPS_HEADER}, + {.path = "gaps.c", .text = C_TU_GAPS_SOURCE}, + {.path = "use_gaps.c", .text = C_TU_GAPS_USE}}; + +TEST(resolver_c_known_gaps_stay_honest) { + const char *use = C_TU_GAPS_USE; + const RQ_Expected expected[] = { + {.case_id = "macro-declared-loop-variable", + .site = C_TU_WORD("use_gaps.c", use, "use_gaps.use_loop", "n += tp->", "tp_next"), + .relation = "USAGE", + .target = {.file = "gaps.h", + .suffix = "gaps.tabpage_S.tp_next", + .label = "Field", + .declaration_line = 4, + .declaration_text = " struct tabpage_S *tp_next;"}, + .mode = RQ_KNOWN_GAP, + .gap_id = "c-macro-declared-variable", + .why = "`FOR_ALL_TABS(tp)` declares tp inside a macro body. The resolver does not " + "expand macros, so tp has no declared type and its members stay unknown.", + .tried = "Read neovim's FOR_ALL_* loops. Guessing the type from the macro text needs " + "expansion rules this resolver does not have."}, + {.case_id = "anonymous-aggregate-member", + .site = C_TU_WORD("use_gaps.c", use, "use_gaps.use_union", "o->value", "value"), + .relation = "USAGE", + .target = {.file = "gaps.h", + .suffix = "gaps.item.value", + .label = "Field", + .declaration_line = 12, + .declaration_text = " int value;"}, + .mode = RQ_KNOWN_GAP, + .gap_id = "c-anonymous-aggregate-member", + .why = "Members of an anonymous struct or union get no Field node, so there is nothing to " + "bind. Neovim's `data` union in Object and redis's `u` unions have this form.", + .tried = "A nested aggregate with a tag binds through the tag. Without one, extraction " + "must first attach the members to the enclosing struct."}, + {.case_id = "function-like-macro", + .site = {.file = "use_gaps.c", .caller = "use_gaps.use_macro", .needle = "LOG_LINE(\"x\")"}, + .relation = "CALLS", + .target = {.file = "gaps.h", + .suffix = "gaps.LOG_LINE#macro", + .label = "Macro", + .declaration_line = 8, + .declaration_text = "#define LOG_LINE(s) log_line(s)"}, + .mode = RQ_KNOWN_GAP, + .gap_id = "c-function-like-macro", + .why = "A function-like macro use reads as a call, and the function it reaches is only " + "known after expansion. `serverLog`, `assert` and `kv_size` have this form.", + .tried = "A macro of the name abstains wherever it is visible, so a same-named function " + "never takes the edge. Binding the Macro node as a call needs a decision on " + "what CALLS means for a macro."}, + }; + const CTUCase fixture = {.suite = "c-tu-gaps", + .sources = C_TU_GAPS, + .source_count = sizeof(C_TU_GAPS) / sizeof(C_TU_GAPS[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .known_gaps = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* `class LLVM_ABI Name {` and `bool f(int c) ATTR_A ATTR_B {` are how real libraries export + * things. Each form is only valid if a word is a macro, and the parse leaves that word out. + * The fixture keeps the macro undefined, as an unconfigured index sees it. */ +#define C_TU_ATTR_HEADER \ + "#ifndef ATTR_H\n" \ + "#define ATTR_H\n" \ + "namespace llvm {\n" \ + "class LLVM_GSL_POINTER StringRef {\n" \ + "public:\n" \ + " LLVM_NODISCARD int size() const { return Length; }\n" \ + " int empty() const LLVM_READONLY { return 0; }\n" \ + " int Length;\n" \ + "};\n" \ + "class LLVM_ABI Derived final : public StringRef {\n" \ + "};\n" \ + "LLVM_NODISCARD StringRef makeRef();\n" \ + "}\n" \ + "#endif\n" +#define C_TU_ATTR_C_HEADER \ + "#ifndef ASCII_H\n" \ + "#define ASCII_H\n" \ + "static inline int ascii_iswhite(int c) FUNC_ATTR_CONST { return c == 32; }\n" \ + "static inline int ascii_isdigit(int c) FUNC_ATTR_CONST FUNC_ATTR_INLINE { return c > 47; }\n" \ + "#endif\n" +#define C_TU_ATTR_USE \ + "#include \"attr.h\"\n" \ + "namespace llvm {\n" \ + "int useMember(StringRef S) { return S.size(); }\n" \ + "int useTrailing(StringRef S) { return S.empty(); }\n" \ + "int useField(StringRef *S) { return S->Length; }\n" \ + "int useBase(Derived D) { return D.size(); }\n" \ + "}\n" +#define C_TU_ATTR_C_USE \ + "#include \"ascii.h\"\n" \ + "int use_one(int c) { return ascii_iswhite(c); }\n" \ + "int use_two(int c) { return ascii_isdigit(c); }\n" +/* `struct timeval tv { 0 };` has the words of the macro form and is a variable, so nothing + * is left out of it. */ +#define C_TU_ATTR_DECOY \ + "struct timeval { int s; };\n" \ + "struct timeval tv { 0 };\n" \ + "int decoy_var(void) { return tv.s; }\n" +static const RQ_Source C_TU_ATTR[] = {{.path = "attr.h", .text = C_TU_ATTR_HEADER}, + {.path = "ascii.h", .text = C_TU_ATTR_C_HEADER}, + {.path = "use_attr.cpp", .text = C_TU_ATTR_USE}, + {.path = "use_ascii.c", .text = C_TU_ATTR_C_USE}, + {.path = "decoy.cpp", .text = C_TU_ATTR_DECOY}}; + +TEST(resolver_c_attribute_macros_do_not_hide_declarations) { + const char *use = C_TU_ATTR_USE; + const char *decoy = C_TU_ATTR_DECOY; + const RQ_Target size = {.file = "attr.h", + .suffix = "llvm.StringRef.size", + .label = "Method", + .declaration_line = 6, + .declaration_text = + " LLVM_NODISCARD int size() const { return Length; }"}; + const RQ_Target empty = {.file = "attr.h", + .suffix = "llvm.StringRef.empty", + .label = "Method", + .declaration_line = 7, + .declaration_text = " int empty() const LLVM_READONLY { return 0; }"}; + const RQ_Target length = {.file = "attr.h", + .suffix = "llvm.StringRef.Length", + .label = "Field", + .declaration_line = 8, + .declaration_text = " int Length;"}; + const RQ_Target string_ref = {.file = "attr.h", + .suffix = "llvm.StringRef", + .label = "Class", + .declaration_line = 4, + .declaration_text = "class LLVM_GSL_POINTER StringRef {"}; + const RQ_Target timeval_type = {.file = "decoy.cpp", + .suffix = "decoy.timeval", + .label = "Class", + .declaration_line = 1, + .declaration_text = "struct timeval { int s; };"}; + const RQ_Target timeval_field = {.file = "decoy.cpp", + .suffix = "decoy.timeval.s", + .label = "Field", + .declaration_line = 1, + .declaration_text = "struct timeval { int s; };"}; + const RQ_Target timeval_var = {.file = "decoy.cpp", + .suffix = "decoy.tv", + .label = "Variable", + .declaration_line = 2, + .declaration_text = "struct timeval tv { 0 };"}; + (void)timeval_type; + const RQ_Expected expected[] = { + {.case_id = "class-with-attribute-macro-has-its-members", + .site = {.file = "use_attr.cpp", .caller = "llvm.useMember", .needle = "S.size()"}, + .relation = "CALLS", + .target = size}, + {.case_id = "trailing-attribute-macro", + .site = {.file = "use_attr.cpp", .caller = "llvm.useTrailing", .needle = "S.empty()"}, + .relation = "CALLS", + .target = empty}, + {.case_id = "class-with-attribute-macro-is-a-class", + .site = C_TU_WORD("use_attr.cpp", use, "llvm.useField", "useField(", "StringRef"), + .relation = "USAGE", + .target = string_ref}, + {.case_id = "field-of-class-with-attribute-macro", + .site = C_TU_WORD("use_attr.cpp", use, "llvm.useField", "S->Length", "Length"), + .relation = "USAGE", + .target = length}, + {.case_id = "base-class-with-attribute-macros", + .site = {.file = "use_attr.cpp", .caller = "llvm.useBase", .needle = "D.size()"}, + .relation = "CALLS", + .target = size}, + {.case_id = "header-definition-one-attribute", + .site = {.file = "use_ascii.c", .caller = "use_ascii.use_one", .needle = "ascii_iswhite(c)"}, + .relation = "CALLS", + .target = {.file = "ascii.h", + .suffix = "ascii.ascii_iswhite", + .label = "Function", + .declaration_line = 3, + .declaration_text = + "static inline int ascii_iswhite(int c) FUNC_ATTR_CONST { return c == 32; }"}}, + {.case_id = "header-definition-two-attributes", + .site = {.file = "use_ascii.c", .caller = "use_ascii.use_two", .needle = "ascii_isdigit(c)"}, + .relation = "CALLS", + .target = {.file = "ascii.h", + .suffix = "ascii.ascii_isdigit", + .label = "Function", + .declaration_line = 4, + .declaration_text = "static inline int ascii_isdigit(int c) FUNC_ATTR_CONST " + "FUNC_ATTR_INLINE { return c > 47; }"}}, + {.case_id = "brace-initialized-variable-stays-a-variable", + .site = C_TU_WORD("decoy.cpp", decoy, "decoy.decoy_var", "return tv", "tv"), + .relation = "USAGE", + .target = timeval_var}, + {.case_id = "brace-initialized-variable-member", + .site = C_TU_WORD("decoy.cpp", decoy, "decoy.decoy_var", "tv.s", "s"), + .relation = "USAGE", + .target = timeval_field}, + }; + const CTUCase fixture = {.suite = "c-tu-attr", + .sources = C_TU_ATTR, + .source_count = sizeof(C_TU_ATTR) / sizeof(C_TU_ATTR[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* The forms LLVM uses for its most-called names: a prototype returning a reference, a + * static member through its class, and a member defined out of line inside a namespace + * block rather than under `using namespace`. */ +#define C_TU_SCOPED_HEADER \ + "#ifndef OSTREAM_H\n" \ + "#define OSTREAM_H\n" \ + "namespace llvm {\n" \ + "class raw_ostream {\n" \ + "public:\n" \ + " raw_ostream &write(int x);\n" \ + "};\n" \ + "raw_ostream &errs();\n" \ + "class Error {\n" \ + "public:\n" \ + " static Error success();\n" \ + "private:\n" \ + " static Error hidden();\n" \ + "};\n" \ + "namespace detail {\n" \ + "class Nested {\n" \ + "public:\n" \ + " int outer();\n" \ + "private:\n" \ + " int inner() const { return 1; }\n" \ + "};\n" \ + "}\n" \ + "}\n" \ + "#endif\n" +#define C_TU_SCOPED_IMPL \ + "#include \"ostream.h\"\n" \ + "namespace llvm {\n" \ + "raw_ostream &errs() { static raw_ostream S; return S; }\n" \ + "raw_ostream &raw_ostream::write(int x) { return *this; }\n" \ + "Error Error::success() { return hidden(); }\n" \ + "Error Error::hidden() { return Error(); }\n" \ + "namespace detail {\n" \ + "int Nested::outer() { return inner(); }\n" \ + "}\n" \ + "}\n" +#define C_TU_SCOPED_USE \ + "#include \"ostream.h\"\n" \ + "using namespace llvm;\n" \ + "void useRef() { errs(); }\n" \ + "void useChain() { errs().write(1); }\n" \ + "Error useStatic() { return Error::success(); }\n" \ + "Error useHidden() { return Error::hidden(); }\n" +static const RQ_Source C_TU_SCOPED[] = {{.path = "lib/ostream.h", .text = C_TU_SCOPED_HEADER}, + {.path = "lib/ostream.cpp", .text = C_TU_SCOPED_IMPL}, + {.path = "lib/use.cpp", .text = C_TU_SCOPED_USE}}; + +TEST(resolver_cpp_reference_prototypes_and_scoped_members) { + const char *use = C_TU_SCOPED_USE; + const RQ_Target errs = {.file = "lib/ostream.cpp", + .suffix = "ostream.llvm.errs", + .label = "Function", + .declaration_line = 3, + .declaration_text = + "raw_ostream &errs() { static raw_ostream S; return S; }"}; + const RQ_Target write = {.file = "lib/ostream.cpp", + .suffix = "ostream.raw_ostream.write", + .label = "Method", + .declaration_line = 4, + .declaration_text = + "raw_ostream &raw_ostream::write(int x) { return *this; }"}; + const RQ_Expected expected[] = { + {.case_id = "prototype-returning-a-reference", + .site = C_TU_WORD("lib/use.cpp", use, "use.useRef", "useRef", "errs()"), + .relation = "CALLS", + .target = errs}, + {.case_id = "chain-starts-at-the-reference", + .site = C_TU_WORD("lib/use.cpp", use, "use.useChain", "useChain", "errs()"), + .relation = "CALLS", + .target = errs}, + {.case_id = "member-of-the-returned-reference", + .site = C_TU_WORD("lib/use.cpp", use, "use.useChain", "useChain", "errs().write(1)"), + .relation = "CALLS", + .target = write}, + {.case_id = "static-member-through-its-class", + .site = {.file = "lib/use.cpp", .caller = "use.useStatic", .needle = "Error::success()"}, + .relation = "CALLS", + .target = {.file = "lib/ostream.cpp", + .suffix = "ostream.Error.success", + .label = "Method", + .declaration_line = 5, + .declaration_text = "Error Error::success() { return hidden(); }"}}, + {.case_id = "private-member-from-inside-its-class", + .site = {.file = "lib/ostream.cpp", .caller = "ostream.Error.success", .needle = "hidden()"}, + .relation = "CALLS", + .target = {.file = "lib/ostream.cpp", + .suffix = "ostream.Error.hidden", + .label = "Method", + .declaration_line = 6, + .declaration_text = "Error Error::hidden() { return Error(); }"}}, + {.case_id = "out-of-line-member-inside-a-namespace-block", + .site = {.file = "lib/ostream.cpp", .caller = "ostream.Nested.outer", .needle = "inner()"}, + .relation = "CALLS", + .target = {.file = "lib/ostream.h", + .suffix = "ostream.llvm.detail.Nested.inner", + .label = "Method", + .declaration_line = 20, + .declaration_text = " int inner() const { return 1; }"}}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "private-static-member-from-outside", + .site = {.file = "lib/use.cpp", .caller = "use.useHidden", .needle = "Error::hidden()"}, + .relation = "CALLS", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-scoped", + .sources = C_TU_SCOPED, + .source_count = sizeof(C_TU_SCOPED) / sizeof(C_TU_SCOPED[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* Inside a member function the class's own names come first, also the ones with no graph + * node to bind: a pure virtual hides a free function of that name. */ +#define C_TU_HIDING_HEADER \ + "#pragma once\n" \ + "// No macro here: the rows below all come from the file as written.\n" \ + "int print(int x);\n" \ + "int render(int x);\n" \ + "int hidden(int x);\n" \ + "class Base {\n" \ + "public:\n" \ + " int render(int x) { return x; }\n" \ + "};\n" \ + "class Printer {\n" \ + "public:\n" \ + " virtual int print(int x) = 0;\n" \ + " int run() { return print(1); }\n" \ + " int free_ok() { return render(1); }\n" \ + "};\n" \ + "class Derived : public Base {\n" \ + "public:\n" \ + " int go() { return render(4); }\n" \ + "};\n" \ + "class FromUnknown : public Unknown {\n" \ + "public:\n" \ + " int go2() { return render(2); }\n" \ + "};\n" \ + "class WithType {\n" \ + " using hidden = int;\n" \ + "public:\n" \ + " int go3() { return hidden(3); }\n" \ + "};\n" +#define C_TU_HIDING_IMPL \ + "#include \"printer.h\"\n" \ + "int print(int x) { return x; }\n" \ + "int render(int x) { return x; }\n" \ + "int hidden(int x) { return x; }\n" +static const RQ_Source C_TU_HIDING[] = {{.path = "printer.h", .text = C_TU_HIDING_HEADER}, + {.path = "printer_impl.cpp", .text = C_TU_HIDING_IMPL}}; + +TEST(resolver_cpp_class_names_hide_the_enclosing_scope) { + const RQ_Expected expected[] = { + {.case_id = "name-the-class-does-not-declare-is-the-free-function", + .site = {.file = "printer.h", .caller = "printer.Printer.free_ok", .needle = "render(1)"}, + .relation = "CALLS", + .target = {.file = "printer_impl.cpp", + .suffix = "printer_impl.render", + .label = "Function", + .declaration_line = 3, + .declaration_text = "int render(int x) { return x; }"}}, + {.case_id = "name-a-known-base-declares", + .site = {.file = "printer.h", .caller = "printer.Derived.go", .needle = "render(4)"}, + .relation = "CALLS", + .target = {.file = "printer.h", + .suffix = "printer.Base.render", + .label = "Method", + .declaration_line = 8, + .declaration_text = " int render(int x) { return x; }"}}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "pure-virtual-hides-the-free-function", + .site = {.file = "printer.h", .caller = "printer.Printer.run", .needle = "print(1)"}, + .relation = "CALLS", + .any_target = true}, + {.case_id = "unknown-base-may-declare-the-name", + .site = {.file = "printer.h", .caller = "printer.FromUnknown.go2", .needle = "render(2)"}, + .relation = "CALLS", + .any_target = true}, + {.case_id = "nested-type-hides-the-free-function", + .site = {.file = "printer.h", .caller = "printer.WithType.go3", .needle = "hidden(3)"}, + .relation = "CALLS", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-hiding", + .sources = C_TU_HIDING, + .source_count = sizeof(C_TU_HIDING) / sizeof(C_TU_HIDING[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* neovim declares every global as `EXTERN win_T *curwin INIT( = NULL);`. */ +#define C_TU_GLOBALS_HEADER \ + "#pragma once\n" \ + "// The way neovim's globals.h is written.\n" \ + "typedef struct window_S win_T;\n" \ + "struct window_S {\n" \ + " int w_cursor;\n" \ + "};\n" \ + "EXTERN win_T *curwin INIT( = NULL);\n" \ + "EXTERN int Rows INIT( = 0);\n" \ + "LUA_API int (lua_gettop) (int *L);\n" +#define C_TU_GLOBALS_USE \ + "#include \"globals.h\"\n" \ + "int read_rows(void) { return Rows; }\n" \ + "void set_cursor(int v) { curwin->w_cursor = v; }\n" \ + "int read_cursor(void) { return curwin->w_cursor; }\n" \ + "int shadowed(int Rows) { return Rows; }\n" \ + "int same(int a, int b) { return (a == b); }\n" \ + "int top(int *L) { return lua_gettop(L); }\n" +static const RQ_Source C_TU_GLOBALS[] = { + {.path = "globals.h", .text = C_TU_GLOBALS_HEADER}, + {.path = "lapi.c", .text = "#include \"globals.h\"\nLUA_API int lua_gettop (int *L) { return *L; }\n"}, + {.path = "use_globals.c", .text = C_TU_GLOBALS_USE}}; + +TEST(resolver_c_initializer_macro_declares_the_global) { + const char *use = C_TU_GLOBALS_USE; + const RQ_Expected expected[] = { + {.case_id = "prototype-with-a-parenthesized-name", + .site = {.file = "use_globals.c", .caller = "use_globals.top", .needle = "lua_gettop(L)"}, + .relation = "CALLS", + .target = {.file = "lapi.c", + .suffix = "lapi.lua_gettop", + .label = "Function", + .declaration_line = 2, + .declaration_text = "LUA_API int lua_gettop (int *L) { return *L; }"}}, + {.case_id = "global-with-initializer-macro", + .site = C_TU_WORD("use_globals.c", use, "use_globals.read_rows", "return Rows", "Rows"), + .relation = "USAGE", + .target = {.file = "globals.h", + .suffix = "globals.Rows", + .label = "Variable", + .declaration_line = 8, + .declaration_text = "EXTERN int Rows INIT( = 0);"}}, + {.case_id = "pointer-global-with-initializer-macro", + .site = C_TU_WORD("use_globals.c", use, "use_globals.read_cursor", "return curwin", "curwin"), + .relation = "USAGE", + .target = {.file = "globals.h", + .suffix = "globals.curwin", + .label = "Variable", + .declaration_line = 7, + .declaration_text = "EXTERN win_T *curwin INIT( = NULL);"}}, + {.case_id = "member-read-through-the-global", + .site = C_TU_WORD("use_globals.c", use, "use_globals.read_cursor", "return curwin->w_cursor", + "w_cursor"), + .relation = "USAGE", + .target = {.file = "globals.h", + .suffix = "globals.window_S.w_cursor", + .label = "Field", + .declaration_line = 5, + .declaration_text = " int w_cursor;"}}, + {.case_id = "member-through-the-global", + .site = C_TU_WORD("use_globals.c", use, "use_globals.set_cursor", "curwin->w_cursor", + "w_cursor"), + .relation = "WRITES", + .target = {.file = "globals.h", + .suffix = "globals.window_S.w_cursor", + .label = "Field", + .declaration_line = 5, + .declaration_text = " int w_cursor;"}}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "parameter-hides-the-global", + .site = C_TU_WORD("use_globals.c", use, "use_globals.shadowed", "return Rows", "Rows"), + .relation = "USAGE", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-globals", + .sources = C_TU_GLOBALS, + .source_count = sizeof(C_TU_GLOBALS) / sizeof(C_TU_GLOBALS[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* A file's proofs all pass through one copy on their way to the graph. A cap there cost a + * large file its last rows, so the call that matters here is the last one in the file. */ +TEST(resolver_c_large_file_keeps_its_last_call) { + enum { CALLS = 30000 }; + const char *head = "int helper(int x) { return x; }\n" + "int last_helper(int x) { return x; }\n" + "int many(void) {\n" + " int n = 0;\n"; + const char *line = " n += helper(0);\n"; + const char *tail = " return n;\n" + "}\n" + "int last_one(void) { return last_helper(1); }\n"; + size_t size = strlen(head) + (size_t)CALLS * strlen(line) + strlen(tail) + 1; + char *text = malloc(size); + ASSERT(text != NULL); + size_t used = 0; + memcpy(text, head, strlen(head)); + used += strlen(head); + for (int i = 0; i < CALLS; i++) { + memcpy(text + used, line, strlen(line)); + used += strlen(line); + } + memcpy(text + used, tail, strlen(tail) + 1); + const RQ_Source sources[] = {{.path = "big.c", .text = text}}; + const RQ_Expected expected[] = { + {.case_id = "last-call-of-a-large-file", + .site = {.file = "big.c", .caller = "big.last_one", .needle = "last_helper(1)"}, + .relation = "CALLS", + .target = {.file = "big.c", + .suffix = "big.last_helper", + .label = "Function", + .declaration_line = 2, + .declaration_text = "int last_helper(int x) { return x; }"}}, + }; + const CTUCase fixture = {.suite = "c-tu-large", + .sources = sources, + .source_count = 1, + .expected = expected, + .expected_count = 1}; + bool ok = true; + for (int driver = 0; driver < 2; driver++) + ok = c_tu_run(&fixture, driver) && ok; + free(text); + ASSERT(ok); + PASS(); +} + +/* What the C++ rules leave unbound on LLVM. Each is a genuine call the resolver abstains on. */ +#define C_TU_CXX_GAPS_HEADER \ + "#ifndef CXX_GAPS_H\n" \ + "#define CXX_GAPS_H\n" \ + "namespace llvm {\n" \ + "template class ArrayRef {\n" \ + "public:\n" \ + " int size() const { return 1; }\n" \ + "};\n" \ + "class Value {\n" \ + "public:\n" \ + " int *parts();\n" \ + " const int *parts() const;\n" \ + " int length() const { return 1; }\n" \ + "};\n" \ + "template bool usesLayout(int x) { return x > 0; }\n" \ + "Value makeValue();\n" \ + "}\n" \ + "#endif\n" +#define C_TU_CXX_GAPS_IMPL \ + "#include \"cxx_gaps.h\"\n" \ + "namespace llvm {\n" \ + "int *Value::parts() { return 0; }\n" \ + "const int *Value::parts() const { return 0; }\n" \ + "}\n" +#define C_TU_CXX_GAPS_USE \ + "#include \"cxx_gaps.h\"\n" \ + "namespace llvm {\n" \ + "int useTemplate(ArrayRef A) { return A.size(); }\n" \ + "int useAuto() { auto V = makeValue(); return V.length(); }\n" \ + "int useConstOverload(Value V) { return *V.parts(); }\n" \ + "bool useFunctionTemplate() { return usesLayout(1); }\n" \ + "}\n" +static const RQ_Source C_TU_CXX_GAPS[] = { + {.path = "cxx_gaps.h", .text = C_TU_CXX_GAPS_HEADER}, + {.path = "cxx_gaps.cpp", .text = C_TU_CXX_GAPS_IMPL}, + {.path = "cxx_gaps_use.cpp", .text = C_TU_CXX_GAPS_USE}}; + +TEST(resolver_cpp_known_gaps_stay_honest) { + const RQ_Expected expected[] = { + {.case_id = "member-of-a-class-template", + .site = {.file = "cxx_gaps_use.cpp", .caller = "llvm.useTemplate", .needle = "A.size()"}, + .relation = "CALLS", + .target = {.file = "cxx_gaps.h", + .suffix = "llvm.ArrayRef.size", + .label = "Method", + .declaration_line = 6, + .declaration_text = " int size() const { return 1; }"}, + .mode = RQ_KNOWN_GAP, + .gap_id = "cpp-class-template-member", + .why = "A specialization can replace the member, and the graph has one node per template " + "name, so a class template is not taken as a receiver's class.", + .tried = "Read SmallVector, ArrayRef and Optional in LLVM. SmallVectorTemplateBase has two " + "specializations; a rule for templates needs specialization facts first."}, + {.case_id = "receiver-declared-auto", + .site = {.file = "cxx_gaps_use.cpp", .caller = "llvm.useAuto", .needle = "V.length()"}, + .relation = "CALLS", + .target = {.file = "cxx_gaps.h", + .suffix = "llvm.Value.length", + .label = "Method", + .declaration_line = 12, + .declaration_text = " int length() const { return 1; }"}, + .mode = RQ_KNOWN_GAP, + .gap_id = "cpp-auto-receiver", + .why = "An `auto` variable's type is inferred from its initializer. Only written " + "declarations give a receiver its type here.", + .tried = "Written declaration types are followed through typedefs, fields and declared " + "return types. Initializer flow is left out on purpose: it can go stale."}, + /* Not a gap: the two definitions are one graph node, so which overload the call + * selects does not change the edge. */ + {.case_id = "overloads-sharing-one-node", + .site = {.file = "cxx_gaps_use.cpp", .caller = "llvm.useConstOverload", .needle = "V.parts()"}, + .relation = "CALLS", + .target = {.file = "cxx_gaps.cpp", + .suffix = "cxx_gaps.Value.parts", + .label = "Method", + .declaration_line = 4, + .declaration_text = "const int *Value::parts() const { return 0; }"}}, + {.case_id = "explicit-template-arguments", + .site = {.file = "cxx_gaps_use.cpp", + .caller = "llvm.useFunctionTemplate", + .needle = "usesLayout(1)"}, + .relation = "CALLS", + .target = {.file = "cxx_gaps.h", + .suffix = "llvm.usesLayout", + .label = "Function", + .declaration_line = 14, + .declaration_text = + "template bool usesLayout(int x) { return x > 0; }"}, + .mode = RQ_KNOWN_GAP, + .gap_id = "cpp-function-template", + .why = "Function templates take part in overload resolution by deduction, which is not " + "modelled.", + .tried = "Plain functions and members bind. A template function is skipped as a candidate " + "so it never wins by being the only name visible."}, + }; + const CTUCase fixture = {.suite = "c-tu-cxx-gaps", + .sources = C_TU_CXX_GAPS, + .source_count = sizeof(C_TU_CXX_GAPS) / sizeof(C_TU_CXX_GAPS[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .known_gaps = sizeof(expected) / sizeof(expected[0]) - 1}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* A precision sample on neovim found `HAS_KEY(opts, win_text_height, start_row)` bound as a + * reference to the function win_text_height. The macro pastes that word into another name. */ +#define C_TU_MACRO_ARGS_HEADER \ + "#ifndef KEYS_H\n" \ + "#define KEYS_H\n" \ + "#define HAS_KEY(d, typ, key) (KEYSET_OPTIDX_##typ##__##key & (d)->is_set__##typ##_)\n" \ + "int win_text_height(int x);\n" \ + "extern int start_row;\n" \ + "#endif\n" +#define C_TU_MACRO_ARGS_IMPL \ + "#include \"keys.h\"\n" \ + "int win_text_height(int x) { return x; }\n" \ + "int start_row;\n" +#define C_TU_MACRO_ARGS_USE \ + "#include \"keys.h\"\n" \ + "int take_callback(int (*cb)(int));\n" \ + "int pasted(int *a) { return HAS_KEY(a, win_text_height, start_row); }\n" \ + "int real_reference(void) { return take_callback(win_text_height); }\n" \ + "int real_value(void) { return start_row; }\n" +static const RQ_Source C_TU_MACRO_ARGS[] = { + {.path = "keys.h", .text = C_TU_MACRO_ARGS_HEADER}, + {.path = "keys.c", .text = C_TU_MACRO_ARGS_IMPL}, + {.path = "use_keys.c", .text = C_TU_MACRO_ARGS_USE}}; + +TEST(resolver_c_bare_macro_arguments_prove_nothing) { + const char *use = C_TU_MACRO_ARGS_USE; + const RQ_Expected expected[] = { + {.case_id = "function-argument-of-a-function", + .site = C_TU_WORD("use_keys.c", use, "use_keys.real_reference", "take_callback(win", + "win_text_height"), + .relation = "CALL_REFERENCE", + .target = {.file = "keys.c", + .suffix = "keys.win_text_height", + .label = "Function", + .declaration_line = 2, + .declaration_text = "int win_text_height(int x) { return x; }"}}, + {.case_id = "value-outside-the-macro", + .site = C_TU_WORD("use_keys.c", use, "use_keys.real_value", "return start_row", "start_row"), + .relation = "USAGE", + .target = {.file = "keys.c", + .suffix = "keys.start_row", + .label = "Variable", + .declaration_line = 3, + .declaration_text = "int start_row;"}}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "pasted-function-name", + .site = C_TU_WORD("use_keys.c", use, "use_keys.pasted", "HAS_KEY(a, ", "win_text_height"), + .relation = "CALL_REFERENCE", + .any_target = true}, + {.case_id = "pasted-value-name", + .site = C_TU_WORD("use_keys.c", use, "use_keys.pasted", "HAS_KEY(a, ", "start_row"), + .relation = "USAGE", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-macro-args", + .sources = C_TU_MACRO_ARGS, + .source_count = sizeof(C_TU_MACRO_ARGS) / sizeof(C_TU_MACRO_ARGS[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* Two forms from LLVM's APFloat and attribute parsers. `typedef detail::IEEEFloat IEEEFloat;` + * inside another class must not compete with the class of that name outside it, and a derived + * class reaches its base's protected members but not its private ones. */ +#define C_TU_ACCESS_HEADER \ + "#ifndef ACCESS_H\n" \ + "#define ACCESS_H\n" \ + "namespace llvm {\n" \ + "namespace detail {\n" \ + "class Inner {\n" \ + "public:\n" \ + " int outer();\n" \ + "private:\n" \ + " int inner() const { return 1; }\n" \ + "};\n" \ + "}\n" \ + "class Wrapper {\n" \ + " typedef detail::Inner Inner;\n" \ + "};\n" \ + "class Parser {\n" \ + "protected:\n" \ + " int parse() { return 1; }\n" \ + "private:\n" \ + " int secret() { return 2; }\n" \ + "};\n" \ + "class ArmParser : public Parser {\n" \ + "public:\n" \ + " int run() { return parse(); }\n" \ + " int peek() { return secret(); }\n" \ + "};\n" \ + "int outside(ArmParser P);\n" \ + "}\n" \ + "#endif\n" +#define C_TU_ACCESS_IMPL \ + "#include \"access.h\"\n" \ + "namespace llvm {\n" \ + "namespace detail {\n" \ + "int Inner::outer() { return inner(); }\n" \ + "}\n" \ + "int outside(ArmParser P) { return P.parse(); }\n" \ + "}\n" +static const RQ_Source C_TU_ACCESS[] = {{.path = "access.h", .text = C_TU_ACCESS_HEADER}, + {.path = "access.cpp", .text = C_TU_ACCESS_IMPL}}; + +TEST(resolver_cpp_nested_aliases_and_protected_members) { + const char *header = C_TU_ACCESS_HEADER; + const RQ_Expected expected[] = { + {.case_id = "alias-inside-another-class-does-not-compete", + .site = {.file = "access.cpp", .caller = "access.Inner.outer", .needle = "inner()"}, + .relation = "CALLS", + .target = {.file = "access.h", + .suffix = "access.llvm.detail.Inner.inner", + .label = "Method", + .declaration_line = 9, + .declaration_text = " int inner() const { return 1; }"}}, + {.case_id = "protected-member-from-a-derived-class", + .site = C_TU_WORD("access.h", header, "access.llvm.ArmParser.run", "return parse", "parse()"), + .relation = "CALLS", + .target = {.file = "access.h", + .suffix = "access.llvm.Parser.parse", + .label = "Method", + .declaration_line = 17, + .declaration_text = " int parse() { return 1; }"}}, + }; + const RQ_Forbidden forbidden[] = { + {.case_id = "private-member-of-a-base", + .site = C_TU_WORD("access.h", header, "access.llvm.ArmParser.peek", "return secret", "secret()"), + .relation = "CALLS", + .any_target = true}, + {.case_id = "protected-member-from-outside", + .site = {.file = "access.cpp", .caller = "llvm.outside", .needle = "P.parse()"}, + .relation = "CALLS", + .any_target = true}, + }; + const CTUCase fixture = {.suite = "c-tu-access", + .sources = C_TU_ACCESS, + .source_count = sizeof(C_TU_ACCESS) / sizeof(C_TU_ACCESS[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0]), + .forbidden = forbidden, + .forbidden_count = sizeof(forbidden) / sizeof(forbidden[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); + PASS(); +} + +/* neovim's typval_T: `rettv->vval.v_number` goes through a union defined where its member + * is declared. Two such unions have a member of one name, so only the tag can tell them apart. */ +#define C_TU_NESTED_HEADER \ + "#ifndef TYPVAL_H\n" \ + "#define TYPVAL_H\n" \ + "typedef struct {\n" \ + " int v_type;\n" \ + " union number_union {\n" \ + " long value;\n" \ + " } vval;\n" \ + " union string_union {\n" \ + " char *value;\n" \ + " } sval;\n" \ + "} typval_T;\n" \ + "#endif\n" +#define C_TU_NESTED_USE \ + "#include \"typval.h\"\n" \ + "void set_number(typval_T *rettv) { rettv->vval.value = 0; }\n" \ + "char *get_string(typval_T tv) { return tv.sval.value; }\n" +static const RQ_Source C_TU_NESTED[] = {{.path = "typval.h", .text = C_TU_NESTED_HEADER}, + {.path = "use_typval.c", .text = C_TU_NESTED_USE}}; + +TEST(resolver_c_member_through_a_union_defined_in_place) { + const char *use = C_TU_NESTED_USE; + const RQ_Expected expected[] = { + {.case_id = "write-through-a-tagged-union", + .site = C_TU_WORD("use_typval.c", use, "use_typval.set_number", "vval.value", "value"), + .relation = "WRITES", + .target = {.file = "typval.h", + .suffix = "typval.number_union.value", + .label = "Field", + .declaration_line = 6, + .declaration_text = " long value;"}}, + {.case_id = "read-through-the-other-tagged-union", + .site = C_TU_WORD("use_typval.c", use, "use_typval.get_string", "sval.value", "value"), + .relation = "USAGE", + .target = {.file = "typval.h", + .suffix = "typval.string_union.value", + .label = "Field", + .declaration_line = 9, + .declaration_text = " char *value;"}}, + {.case_id = "the-member-holding-the-union", + .site = C_TU_WORD("use_typval.c", use, "use_typval.get_string", "tv.sval", "sval"), + .relation = "USAGE", + .target = {.file = "typval.h", + .suffix = "typval.typval_T.sval", + .label = "Field", + .declaration_line = 8, + .declaration_text = " union string_union {"}}, + {.case_id = "the-parameter-type", + .site = C_TU_WORD("use_typval.c", use, "use_typval.get_string", "get_string(typval_T", "typval_T"), + .relation = "USAGE", + .target = {.file = "typval.h", + .suffix = "typval.typval_T", + .label = "Class", + .declaration_line = 3, + .declaration_text = "typedef struct {"}}, + }; + const CTUCase fixture = {.suite = "c-tu-nested", + .sources = C_TU_NESTED, + .source_count = sizeof(C_TU_NESTED) / sizeof(C_TU_NESTED[0]), + .expected = expected, + .expected_count = sizeof(expected) / sizeof(expected[0])}; + for (int driver = 0; driver < 2; driver++) + ASSERT(c_tu_run(&fixture, driver)); PASS(); } @@ -1158,6 +2935,25 @@ TEST(resolver_c_file_past_the_proof_limit_does_not_stop_publication) { SUITE(resolver_c) { RUN_TEST(resolver_c_file_past_the_proof_limit_does_not_stop_publication); RUN_TEST(resolver_c_macro_include_proves_nothing_for_its_file); + RUN_TEST(resolver_c_member_through_a_union_defined_in_place); + RUN_TEST(resolver_cpp_nested_aliases_and_protected_members); + RUN_TEST(resolver_c_bare_macro_arguments_prove_nothing); + RUN_TEST(resolver_cpp_known_gaps_stay_honest); + RUN_TEST(resolver_c_large_file_keeps_its_last_call); + RUN_TEST(resolver_c_initializer_macro_declares_the_global); + RUN_TEST(resolver_cpp_class_names_hide_the_enclosing_scope); + RUN_TEST(resolver_cpp_reference_prototypes_and_scoped_members); + RUN_TEST(resolver_c_attribute_macros_do_not_hide_declarations); + RUN_TEST(resolver_c_known_gaps_stay_honest); + RUN_TEST(resolver_cpp_members_and_namespaces_bind_through_the_translation_unit); + RUN_TEST(resolver_c_header_prototype_binds_external_definition_through_includes); + RUN_TEST(resolver_c_lookalike_calls_abstain); + RUN_TEST(resolver_c_copied_library_binds_the_copy_compiled_against_its_header); + RUN_TEST(resolver_c_configured_include_root_reaches_calls); + RUN_TEST(resolver_c_unique_include_spelling_reaches_calls_without_configuration); + RUN_TEST(resolver_c_generated_declaration_header_exposes_its_source); + RUN_TEST(resolver_c_member_access_follows_written_declarations); + RUN_TEST(resolver_c_bare_values_and_types_bind_in_the_translation_unit); RUN_TEST(resolver_c_nested_syntax_error_bounds_proofs_at_the_error); RUN_TEST(resolver_c_preprocessed_proofs_require_complete_explicit_configuration); RUN_TEST(resolver_c_unverified_preprocessing_cannot_persist_relationships);