From c650292433534127b5f45edd066e56a06b3376d1 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 25 Sep 2026 18:49:37 +0200 Subject: [PATCH 01/49] feat(identity): callable identity plumbing, no language enabled (#2061) Overloads that share a base QN (Java/C#/C++/Kotlin/Swift/Scala/ObjC) merge into one graph node today, and that node collects the CALLS of every overload. The agreed cure is one signature-qualified callable identity for all overloading languages: callable_qn := base_qn [] (params) [cvref] This change lands the plumbing with every language still at CBM_CALLABLE_ID_NONE, so the graph is byte-identical; each language is switched on later in its own change (with its index-format bump). - internal/cbm/callable_sig.{c,h}: the per-language mode side table (NONE|TYPED|LABELED_TYPED|LABELED|ARITY; kept out of CBMLangSpec like cbm_string_dispatch_suffixes, since ~160 positional rows would otherwise all need the member under -Wmissing-field-initializers), the shared builder cbm_callable_sig[_mode] for the def and the call-scope QN, and the inverse cbm_qn_callable_base_len[_named]. Suffix contract: no '.', '::' or '->'; whitespace-normalized; comments/annotations/attributes dropped; qualified types keep their last segment; '->' spelled '=>'; variadic '[]' (Java) / '~'; C++ top-level cv dropped, arrays decay, cvref kept; <= 200 bytes, longer ones keep whole leading entries and end in '#)'. The builder is iterative (cursor walk, bounded declarator loop) and self-checks against the inverse. - The inverse is anchored to the node's bare name wherever the name is known: real QNs already end in ')' without being callables (a Java field whose name is its declarator `X = f()`, make `$(VAR)` targets), and only the anchor makes the inverse exact on them. - Leaf splitters strip a suffix first (no-op today): registry by-name key (indexed by the bare name for a suffixed callable), cbm_lsp_bare_segment, cbm_pipeline_qn_class_method_tail, pxc_last_component, pxc_qn_leaf, go_index_visit_node, collect_prod_defs, sg_qn_prefix_len. - New unused fields: CBMDefinition.qn_sig_off, CBMResolvedCall.callee_sig (relocated by result compaction), CBMRegisteredFunc.sig, CBMLSPDef.sig. The lsp_surface codec is unchanged (no format bump in this change). - get_code_snippet and trace_path gain base-QN tiers (cbm_store_find_nodes_by_qn_base) that find nothing until suffixes exist. - store: cbm_store_find_nodes_by_qn_suffix built its LIKE pattern in a 512-byte buffer (a long suffix was truncated into a pattern that matched nothing) and did not escape '_'/'%' (`my_func` also matched `myXfunc`). The pattern is now heap-built, escaped, and matched with ESCAPE '\'. Proof: full sorted node+edge+lsp_surface dumps are byte-identical before and after on 7 corpora (this repo, elasticsearch libs, Exposed, dotnet System.Linq, System.Collections, coreclr/gc, a C corpus). Refs #2061 Signed-off-by: Martin Vogel --- Makefile.cbm | 2 + internal/cbm/callable_sig.c | 1192 ++++++++++++++++++++++++++ internal/cbm/callable_sig.h | 83 ++ internal/cbm/cbm.h | 8 + internal/cbm/lsp/go_lsp.h | 3 + internal/cbm/lsp/type_registry.h | 3 + internal/cbm/result_compact.c | 1 + scripts/memory-core-baseline.txt | 2 +- src/mcp/mcp.c | 126 ++- src/pipeline/lsp_resolve.h | 16 +- src/pipeline/pass_ensemble_routing.c | 10 +- src/pipeline/pass_lsp_cross.c | 17 +- src/pipeline/pass_semantic.c | 10 +- src/pipeline/registry.c | 10 +- src/store/store.c | 218 ++++- src/store/store.h | 7 + tests/test_callable_sig.c | 348 ++++++++ tests/test_main.c | 2 + tests/test_store_nodes.c | 98 +++ 19 files changed, 2072 insertions(+), 84 deletions(-) create mode 100644 internal/cbm/callable_sig.c create mode 100644 internal/cbm/callable_sig.h create mode 100644 tests/test_callable_sig.c diff --git a/Makefile.cbm b/Makefile.cbm index 305cb69372..a58af40b36 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -337,6 +337,7 @@ FOUNDATION_SRCS = \ EXTRACTION_SRCS = \ $(CBM_DIR)/cbm.c \ $(CBM_DIR)/extract_defs.c \ + $(CBM_DIR)/callable_sig.c \ $(CBM_DIR)/extract_calls.c \ $(CBM_DIR)/extract_imports.c \ $(CBM_DIR)/extract_usages.c \ @@ -630,6 +631,7 @@ TEST_FOUNDATION_SRCS = \ TEST_EXTRACTION_SRCS = \ tests/test_extraction.c \ + tests/test_callable_sig.c \ tests/test_extraction_inheritance.c \ tests/test_extraction_imports.c \ tests/test_parse_coverage.c \ diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c new file mode 100644 index 0000000000..9d32f391a7 --- /dev/null +++ b/internal/cbm/callable_sig.c @@ -0,0 +1,1192 @@ +/* + * callable_sig.c — see callable_sig.h for the identity contract (#2061). + * + * The builder renders the suffix into a growable arena buffer, then applies + * the spelling rules in a single character pass, then the length cap. The result is re-checked + * against cbm_qn_callable_base_len so a malformed parse can never mint a suffix the leaf splitters + * would misread: such a suffix degrades to its hashed form. + */ +#include "callable_sig.h" +#include "helpers.h" +#include +#include +#include +#include +#include + +enum { + SIG_DEPTH_LIMIT = 64, /* type subtree recursion bound */ + SIG_DECL_DEPTH = 8, /* C-family declarator chain bound */ + SIG_MAX_ENTRIES = 256, /* entries tracked for the cap; more => capped */ + SIG_INITIAL_CAP = 256, /* first render buffer; grows by doubling */ + SIG_HASH_HEX = 16, /* FNV-1a 64 rendered as hex */ + SIG_OPERATOR_LEN = 8, /* strlen("operator") */ + SIG_CONST_LEN = 5, /* strlen("const") */ + SIG_VOLATILE_LEN = 8, /* strlen("volatile") */ + SIG_ARITY_DIGITS = 16, /* "(%d)" scratch */ + SIG_CAPPED_TAIL = 1 + SIG_HASH_HEX + 1, /* "#" hex ")" */ +}; + +static const uint64_t SIG_FNV_OFFSET = 0xcbf29ce484222325ULL; /* FNV-1a 64 basis */ +static const uint64_t SIG_FNV_PRIME = 0x100000001b3ULL; +static const char SIG_OPERATOR_CHARS[] = "+-*/%^&|~!=<>?[]"; + +CBMCallableIdentity cbm_callable_identity(CBMLanguage lang) { + /* Every language keeps its historical QN until its enable change lands + * (with the index-format bump that change carries). The planned modes: + * Java/Kotlin/C#/C++/CUDA/Scala TYPED, Swift LABELED_TYPED, ObjC LABELED, + * dynamic tier-2 languages ARITY. */ + (void)lang; + return CBM_CALLABLE_ID_NONE; +} + +/* ── Output buffer ─────────────────────────────────────────────── */ + +typedef struct { + CBMArena *arena; + const char *src; + CBMLanguage lang; + CBMCallableIdentity mode; + char *buf; + size_t len; + size_t cap; + bool overflow; + int entries; + bool too_many; + size_t entry_end[SIG_MAX_ENTRIES]; + size_t open_off; /* offset of the parameter list's "(" */ + size_t close_off; /* offset just past its ")" */ + bool skip_names; /* nested C parameter lists: leave out parameter names */ +} sig_ctx_t; + +static bool sig_ident_char(unsigned char ch) { + return isalnum(ch) || ch == '_' || ch == '$' || ch >= 0x80; +} + +/* Grow by doubling inside the arena; the abandoned copies are bounded by the + * final size (geometric), and the arena is the extraction's scratch anyway. */ +static void sig_raw(sig_ctx_t *c, const char *s, size_t n) { + if (c->overflow) { + return; + } + if (c->len + n >= c->cap) { + size_t cap = c->cap ? c->cap : SIG_INITIAL_CAP; + while (c->len + n >= cap) { + cap *= 2; + } + char *grown = cbm_arena_alloc(c->arena, cap); + if (!grown) { + c->overflow = true; + return; + } + memcpy(grown, c->buf, c->len); + c->buf = grown; + c->cap = cap; + } + memcpy(c->buf + c->len, s, n); + c->len += n; + c->buf[c->len] = '\0'; +} + +static void sig_raw_str(sig_ctx_t *c, const char *s) { + sig_raw(c, s, strlen(s)); +} + +/* Append one source token: internal whitespace is dropped except as a single + * space between two identifier characters, which is also the rule at the + * boundary with what is already in the buffer. */ +static void sig_token(sig_ctx_t *c, const char *s, size_t n) { + bool pending_space = false; + for (size_t i = 0; i < n; i++) { + unsigned char ch = (unsigned char)s[i]; + if (isspace(ch)) { + pending_space = true; + continue; + } + bool first = c->len == 0 || c->buf[c->len - 1] == ' '; + if (!first && sig_ident_char((unsigned char)c->buf[c->len - 1]) && sig_ident_char(ch) && + (pending_space || i == 0)) { + sig_raw(c, " ", 1); + } + pending_space = false; + sig_raw(c, (const char *)&s[i], 1); + } +} + +static const char *sig_variadic_marker(CBMLanguage lang) { + return lang == CBM_LANG_JAVA ? "[]" : "~"; +} + +/* ── Type spelling ─────────────────────────────────────────────── */ + +static bool sig_skipped_kind(const char *kind) { + return strstr(kind, "comment") != NULL || strcmp(kind, "annotation") == 0 || + strcmp(kind, "marker_annotation") == 0 || strcmp(kind, "attribute_list") == 0 || + strcmp(kind, "attribute") == 0 || strcmp(kind, "attribute_declaration") == 0 || + strcmp(kind, "attribute_specifier") == 0; +} + +/* Whether a leaf is emitted: C-family nested parameter lists spell their + * declarators without the parameter names. */ +static void sig_leaf(sig_ctx_t *c, TSNode n) { + const char *kind = ts_node_type(n); + if (c->skip_names && + (strcmp(kind, "identifier") == 0 || strcmp(kind, "field_identifier") == 0)) { + return; + } + uint32_t s = ts_node_start_byte(n); + uint32_t e = ts_node_end_byte(n); + if (e - s == 3 && memcmp(c->src + s, "...", 3) == 0) { + sig_raw_str(c, sig_variadic_marker(c->lang)); + return; + } + sig_token(c, c->src + s, e - s); +} + +/* Render every leaf token of a type subtree, in order, with a cursor walk + * bounded at SIG_DEPTH_LIMIT. C# tuple element names are not part of the + * tuple's type identity and are skipped; Swift tuple labels are, and stay. */ +static void sig_type_tokens(sig_ctx_t *c, TSNode root) { + if (ts_node_is_null(root)) { + return; + } + bool in_tuple_element[SIG_DEPTH_LIMIT + 1] = {false}; + TSTreeCursor cur = ts_tree_cursor_new(root); + int depth = 0; + for (;;) { + TSNode n = ts_tree_cursor_current_node(&cur); + const char *kind = ts_node_type(n); + const char *field = depth > 0 ? ts_tree_cursor_current_field_name(&cur) : NULL; + bool skip = sig_skipped_kind(kind) || + (in_tuple_element[depth] && field && strcmp(field, "name") == 0); + if (!skip && ts_node_child_count(n) == 0) { + sig_leaf(c, n); + } else if (!skip && depth < SIG_DEPTH_LIMIT && ts_tree_cursor_goto_first_child(&cur)) { + depth++; + in_tuple_element[depth] = strcmp(kind, "tuple_element") == 0; + continue; + } + while (depth > 0 && !ts_tree_cursor_goto_next_sibling(&cur)) { + ts_tree_cursor_goto_parent(&cur); + depth--; + } + if (depth == 0) { + break; + } + } + ts_tree_cursor_delete(&cur); +} + +/* ── Entries ───────────────────────────────────────────────────── */ + +static bool sig_writes_types(const sig_ctx_t *c) { + return c->mode == CBM_CALLABLE_ID_TYPED || c->mode == CBM_CALLABLE_ID_LABELED_TYPED; +} + +static bool sig_writes_labels(const sig_ctx_t *c) { + return c->mode == CBM_CALLABLE_ID_LABELED_TYPED || c->mode == CBM_CALLABLE_ID_LABELED; +} + +static void sig_entry_open(sig_ctx_t *c) { + if (c->entries > 0 && sig_writes_types(c)) { + sig_raw(c, ",", 1); + } +} + +static void sig_entry_close(sig_ctx_t *c) { + if (c->entries < SIG_MAX_ENTRIES) { + c->entry_end[c->entries] = c->len; + } else { + c->too_many = true; + } + c->entries++; +} + +/* label (NULL = none -> "_") followed by ':' in the labeled modes. */ +static void sig_label(sig_ctx_t *c, const char *label, size_t n) { + if (!sig_writes_labels(c)) { + return; + } + if (label && n > 0) { + sig_raw(c, label, n); + } else { + sig_raw(c, "_", 1); + } + sig_raw(c, ":", 1); +} + +static void sig_label_node(sig_ctx_t *c, TSNode n) { + if (ts_node_is_null(n)) { + sig_label(c, NULL, 0); + return; + } + uint32_t s = ts_node_start_byte(n); + sig_label(c, c->src + s, ts_node_end_byte(n) - s); +} + +/* An entry holding one plain type node (NULL node -> "?"). */ +static void sig_entry_type(sig_ctx_t *c, TSNode type, const char *suffix) { + sig_entry_open(c); + sig_label(c, NULL, 0); + if (sig_writes_types(c)) { + size_t before = c->len; + sig_type_tokens(c, type); + if (c->len == before) { + sig_raw(c, "?", 1); + } + if (suffix) { + sig_raw_str(c, suffix); + } + } + sig_entry_close(c); +} + +static TSNode sig_field(TSNode n, const char *field) { + return ts_node_child_by_field_name(n, field, (uint32_t)strlen(field)); +} + +static bool sig_node_text_is(const sig_ctx_t *c, TSNode n, const char *text) { + uint32_t s = ts_node_start_byte(n); + uint32_t e = ts_node_end_byte(n); + size_t len = strlen(text); + return e - s == len && memcmp(c->src + s, text, len) == 0; +} + +/* ── Java ──────────────────────────────────────────────────────── */ + +static void sig_java_params(sig_ctx_t *c, TSNode node) { + TSNode params = sig_field(node, "parameters"); + uint32_t count = ts_node_is_null(params) ? 0 : ts_node_named_child_count(params); + for (uint32_t i = 0; i < count; i++) { + TSNode p = ts_node_named_child(params, i); + const char *kind = ts_node_type(p); + if (strcmp(kind, "formal_parameter") == 0) { + sig_entry_open(c); + sig_label(c, NULL, 0); + if (sig_writes_types(c)) { + size_t before = c->len; + sig_type_tokens(c, sig_field(p, "type")); + if (c->len == before) { + sig_raw(c, "?", 1); + } + sig_type_tokens(c, sig_field(p, "dimensions")); /* C-style int a[] */ + } + sig_entry_close(c); + } else if (strcmp(kind, "spread_parameter") == 0) { + TSNode type = {0}; + uint32_t pc = ts_node_named_child_count(p); + for (uint32_t j = 0; j < pc; j++) { + TSNode ch = ts_node_named_child(p, j); + const char *ck = ts_node_type(ch); + if (strcmp(ck, "modifiers") != 0 && strcmp(ck, "variable_declarator") != 0 && + !sig_skipped_kind(ck)) { + type = ch; + break; + } + } + sig_entry_type(c, type, sig_variadic_marker(c->lang)); + } + /* receiver_parameter (`Foo this`) is not part of the signature. */ + } +} + +/* ── C# ────────────────────────────────────────────────────────── */ + +static void sig_cs_param(sig_ctx_t *c, TSNode p) { + sig_entry_open(c); + sig_label(c, NULL, 0); + if (sig_writes_types(c)) { + uint32_t pc = ts_node_named_child_count(p); + for (uint32_t j = 0; j < pc; j++) { + TSNode m = ts_node_named_child(p, j); + if (strcmp(ts_node_type(m), "modifier") == 0 && + (sig_node_text_is(c, m, "ref") || sig_node_text_is(c, m, "out") || + sig_node_text_is(c, m, "in") || sig_node_text_is(c, m, "readonly"))) { + sig_type_tokens(c, m); + } + } + size_t before = c->len; + sig_type_tokens(c, sig_field(p, "type")); + if (c->len == before) { + sig_raw(c, "?", 1); + } + } + sig_entry_close(c); +} + +static void sig_cs_params(sig_ctx_t *c, TSNode node) { + TSNode params = sig_field(node, "parameters"); + uint32_t count = ts_node_is_null(params) ? 0 : ts_node_child_count(params); + for (uint32_t i = 0; i < count; i++) { + TSNode p = ts_node_child(params, i); + const char *field = ts_node_field_name_for_child(params, i); + if (strcmp(ts_node_type(p), "parameter") == 0) { + sig_cs_param(c, p); + } else if (field && strcmp(field, "type") == 0) { + /* `params T[] rest`: the grammar inlines the parameter's type and + * name into the list; `params` itself does not change identity. */ + sig_entry_type(c, p, NULL); + } + } +} + +static void sig_cs_tparams(sig_ctx_t *c, TSNode node) { + TSNode tp = sig_field(node, "type_parameters"); + uint32_t count = ts_node_is_null(tp) ? 0 : ts_node_named_child_count(tp); + int written = 0; + for (uint32_t i = 0; i < count; i++) { + TSNode p = ts_node_named_child(tp, i); + if (strcmp(ts_node_type(p), "type_parameter") != 0) { + continue; + } + sig_raw(c, written ? "," : "<", 1); + sig_type_tokens(c, sig_field(p, "name")); + written++; + } + if (written) { + sig_raw(c, ">", 1); + } +} + +/* ── C / C++ ───────────────────────────────────────────────────── */ + +static TSNode sig_c_inner_declarator(TSNode d) { + TSNode next = sig_field(d, "declarator"); + if (!ts_node_is_null(next)) { + return next; + } + uint32_t count = ts_node_named_child_count(d); + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_named_child(d, i); + if (strstr(ts_node_type(ch), "declarator") != NULL) { + return ch; + } + } + return (TSNode){0}; +} + +static bool sig_is_c_decl_kind(const char *k) { + return strcmp(k, "function_definition") == 0 || strcmp(k, "declaration") == 0 || + strcmp(k, "field_declaration") == 0 || strcmp(k, "template_declaration") == 0; +} + +/* The function_declarator of a C-family definition/declaration, looking + * through (nested) template_declarations and pointer/reference return + * declarators. */ +static TSNode sig_c_function_declarator(TSNode node) { + TSNode cur = node; + for (int depth = 0; depth < SIG_DECL_DEPTH && !ts_node_is_null(cur) && + strcmp(ts_node_type(cur), "template_declaration") == 0; + depth++) { + TSNode inner = {0}; + uint32_t count = ts_node_named_child_count(cur); + for (uint32_t i = 0; i < count && ts_node_is_null(inner); i++) { + TSNode ch = ts_node_named_child(cur, i); + if (sig_is_c_decl_kind(ts_node_type(ch))) { + inner = ch; + } + } + cur = inner; + } + for (int depth = 0; depth < SIG_DECL_DEPTH && !ts_node_is_null(cur); depth++) { + if (strcmp(ts_node_type(cur), "function_declarator") == 0) { + return cur; + } + cur = sig_c_inner_declarator(cur); + } + return (TSNode){0}; +} + +static void sig_c_qualifiers(sig_ctx_t *c, TSNode n) { + uint32_t count = ts_node_named_child_count(n); + for (uint32_t i = 0; i < count; i++) { + TSNode q = ts_node_named_child(n, i); + if (strcmp(ts_node_type(q), "type_qualifier") == 0) { + sig_type_tokens(c, q); + } + } +} + +static bool sig_c_has_qualifier(TSNode n) { + uint32_t count = ts_node_named_child_count(n); + for (uint32_t i = 0; i < count; i++) { + if (strcmp(ts_node_type(ts_node_named_child(n, i)), "type_qualifier") == 0) { + return true; + } + } + return false; +} + +static bool sig_c_is_name(TSNode d) { + if (ts_node_is_null(d)) { + return true; + } + const char *k = ts_node_type(d); + return strstr(k, "declarator") == NULL; +} + +static bool sig_c_is_param_kind(const char *k) { + return strcmp(k, "parameter_declaration") == 0 || + strcmp(k, "optional_parameter_declaration") == 0 || + strcmp(k, "variadic_parameter_declaration") == 0; +} + +static bool sig_c_is_void_list(const sig_ctx_t *c, TSNode params) { + if (ts_node_named_child_count(params) != 1) { + return false; + } + TSNode p = ts_node_named_child(params, 0); + TSNode type = sig_field(p, "type"); + return strcmp(ts_node_type(p), "parameter_declaration") == 0 && !ts_node_is_null(type) && + sig_node_text_is(c, type, "void") && ts_node_is_null(sig_field(p, "declarator")) && + !sig_c_has_qualifier(p); +} + +/* The parameter list of a function-typed parameter (`int (*cb)(int x)`), + * spelled from its tokens without parameter names or default values. */ +static void sig_c_nested_params(sig_ctx_t *c, TSNode fdecl) { + TSNode params = sig_field(fdecl, "parameters"); + sig_raw(c, "(", 1); + uint32_t count = ts_node_is_null(params) ? 0 : ts_node_child_count(params); + bool void_list = !ts_node_is_null(params) && sig_c_is_void_list(c, params); + int written = 0; + bool saved = c->skip_names; + c->skip_names = true; + for (uint32_t i = 0; i < count && !void_list; i++) { + TSNode p = ts_node_child(params, i); + bool is_ellipsis = !ts_node_is_named(p) && sig_node_text_is(c, p, "..."); + if (!is_ellipsis && !sig_c_is_param_kind(ts_node_type(p))) { + continue; + } + if (written++) { + sig_raw(c, ",", 1); + } + if (is_ellipsis) { + sig_raw_str(c, sig_variadic_marker(c->lang)); + continue; + } + sig_c_qualifiers(c, p); + sig_type_tokens(c, sig_field(p, "type")); + sig_type_tokens(c, sig_field(p, "declarator")); + } + c->skip_names = saved; + sig_raw(c, ")", 1); +} + +/* Declarator markers, outermost first: `*` (with its qualifiers unless it is + * the top-level one, which the language drops), `&`/`&&`, an array that + * decays to `*` at the top level, a pack/variadic as the variadic marker. + * A function declarator opens "(" and is closed — with its parameter list — + * after everything inside it, innermost first; one with nothing inside is a + * function parameter decaying to a pointer. Returns whether any marker was + * written, so the caller knows whether base cv-qualifiers are top-level. */ +static bool sig_c_markers(sig_ctx_t *c, TSNode d) { + TSNode pending[SIG_DECL_DEPTH]; + size_t pending_at[SIG_DECL_DEPTH]; + int npending = 0; + bool wrote = false; + for (int depth = 0; !sig_c_is_name(d) && depth < SIG_DECL_DEPTH; depth++) { + const char *k = ts_node_type(d); + TSNode inner = sig_c_inner_declarator(d); + if (strstr(k, "pointer_declarator") != NULL) { + sig_raw(c, "*", 1); + if (!sig_c_is_name(inner)) { + sig_c_qualifiers(c, d); /* not top-level: qualifiers are identity */ + } + } else if (strstr(k, "reference_declarator") != NULL) { + TSNode op = ts_node_child(d, 0); + bool rvalue = + !ts_node_is_null(op) && !ts_node_is_named(op) && sig_node_text_is(c, op, "&&"); + sig_raw_str(c, rvalue ? "&&" : "&"); + } else if (strstr(k, "array_declarator") != NULL) { + sig_raw_str(c, sig_c_is_name(inner) ? "*" : "[]"); + } else if (strstr(k, "function_declarator") != NULL) { + sig_raw(c, "(", 1); + pending[npending] = d; + pending_at[npending] = c->len; + npending++; + } else if (strcmp(k, "variadic_declarator") == 0) { + sig_raw_str(c, sig_variadic_marker(c->lang)); + } else { + d = inner; /* parenthesized_declarator and friends add nothing */ + continue; + } + wrote = true; + d = inner; + } + while (npending > 0) { + npending--; + if (c->len == pending_at[npending]) { + sig_raw(c, "*", 1); /* a function parameter decays to a pointer */ + } + sig_raw(c, ")", 1); + sig_c_nested_params(c, pending[npending]); + } + return wrote; +} + +/* One C-family parameter's type: [non-top-level cv] base-type markers. */ +static void sig_c_param_type(sig_ctx_t *c, TSNode p) { + TSNode declarator = sig_field(p, "declarator"); + /* Probe whether the declarator adds markers, then render in order: + * top-level base cv (no markers) is dropped like the language does. */ + size_t mark = c->len; + bool has_markers = sig_c_markers(c, declarator); + c->len = mark; + c->buf[c->len] = '\0'; + if (has_markers) { + sig_c_qualifiers(c, p); + } + TSNode type = sig_field(p, "type"); + const char *tk = ts_node_is_null(type) ? "" : ts_node_type(type); + TSNode tag_name = sig_field(type, "name"); + bool elaborated = (strcmp(tk, "struct_specifier") == 0 || strcmp(tk, "class_specifier") == 0 || + strcmp(tk, "union_specifier") == 0 || strcmp(tk, "enum_specifier") == 0) && + !ts_node_is_null(tag_name) && ts_node_is_null(sig_field(type, "body")); + size_t before = c->len; + sig_type_tokens(c, elaborated ? tag_name : type); + if (c->len == before) { + sig_raw(c, "?", 1); + } + (void)sig_c_markers(c, declarator); +} + +/* "(" entries ")" of the callable's own function_declarator. */ +static void sig_c_param_list(sig_ctx_t *c, TSNode fdecl) { + TSNode params = sig_field(fdecl, "parameters"); + c->open_off = c->len; + sig_raw(c, "(", 1); + uint32_t count = ts_node_is_null(params) ? 0 : ts_node_child_count(params); + bool void_list = !ts_node_is_null(params) && sig_c_is_void_list(c, params); + for (uint32_t i = 0; i < count && !void_list; i++) { + TSNode p = ts_node_child(params, i); + bool is_ellipsis = !ts_node_is_named(p) && sig_node_text_is(c, p, "..."); + if (!is_ellipsis && !sig_c_is_param_kind(ts_node_type(p))) { + continue; + } + sig_entry_open(c); + sig_label(c, NULL, 0); + if (sig_writes_types(c)) { + if (is_ellipsis) { + sig_raw_str(c, sig_variadic_marker(c->lang)); + } else { + sig_c_param_type(c, p); + } + } + sig_entry_close(c); + } + sig_raw(c, ")", 1); + c->close_off = c->len; +} + +static void sig_cpp_tparams(sig_ctx_t *c, TSNode node) { + TSNode tmpl = node; + if (strcmp(ts_node_type(tmpl), "template_declaration") != 0) { + tmpl = ts_node_parent(node); + if (ts_node_is_null(tmpl) || strcmp(ts_node_type(tmpl), "template_declaration") != 0) { + return; + } + } + TSNode list = sig_field(tmpl, "parameters"); + uint32_t count = ts_node_is_null(list) ? 0 : ts_node_named_child_count(list); + sig_raw(c, "<", 1); + int written = 0; + for (uint32_t i = 0; i < count; i++) { + TSNode p = ts_node_named_child(list, i); + const char *k = ts_node_type(p); + if (sig_skipped_kind(k)) { + continue; + } + if (written++) { + sig_raw(c, ",", 1); + } + /* Template identity depends on parameter kinds, not their names. */ + if (strcmp(k, "type_parameter_declaration") == 0 || + strcmp(k, "optional_type_parameter_declaration") == 0) { + sig_raw_str(c, "typename"); + } else if (strcmp(k, "variadic_type_parameter_declaration") == 0) { + sig_raw_str(c, "typename"); + sig_raw_str(c, sig_variadic_marker(c->lang)); + } else if (strcmp(k, "template_template_parameter_declaration") == 0) { + sig_raw_str(c, "template"); + } else { + sig_c_param_type(c, p); + } + } + sig_raw(c, ">", 1); +} + +/* `template void S::f()`: the template parameters belong to the + * class, and the in-class declaration `void f();` carries none — both must + * spell the same identity. */ +static bool sig_cpp_member_of_class_template(TSNode fdecl) { + TSNode name = ts_node_is_null(fdecl) ? fdecl : sig_field(fdecl, "declarator"); + for (int depth = 0; depth < SIG_DECL_DEPTH && !ts_node_is_null(name) && + strcmp(ts_node_type(name), "qualified_identifier") == 0; + depth++) { + TSNode scope = sig_field(name, "scope"); + if (!ts_node_is_null(scope) && strcmp(ts_node_type(scope), "template_type") == 0) { + return true; + } + name = sig_field(name, "name"); + } + return false; +} + +static void sig_cpp_cvref(sig_ctx_t *c, TSNode fdecl) { + uint32_t count = ts_node_named_child_count(fdecl); + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_named_child(fdecl, i); + const char *k = ts_node_type(ch); + if (strcmp(k, "type_qualifier") == 0 || strcmp(k, "ref_qualifier") == 0) { + sig_type_tokens(c, ch); + } + } +} + +/* ── Kotlin ────────────────────────────────────────────────────── */ + +static bool sig_kotlin_is_vararg(const sig_ctx_t *c, TSNode mods) { + uint32_t count = ts_node_named_child_count(mods); + for (uint32_t i = 0; i < count; i++) { + TSNode m = ts_node_named_child(mods, i); + if (strcmp(ts_node_type(m), "parameter_modifier") == 0 && + sig_node_text_is(c, m, "vararg")) { + return true; + } + } + return false; +} + +static void sig_kotlin_param(sig_ctx_t *c, TSNode p, bool vararg) { + sig_entry_open(c); + sig_label(c, NULL, 0); + if (sig_writes_types(c)) { + size_t before = c->len; + uint32_t count = ts_node_named_child_count(p); + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_named_child(p, i); + if (strcmp(ts_node_type(ch), "simple_identifier") != 0) { + sig_type_tokens(c, ch); /* type_modifiers + the type */ + } + } + if (c->len == before) { + sig_raw(c, "?", 1); + } + if (vararg) { + sig_raw_str(c, sig_variadic_marker(c->lang)); + } + } + sig_entry_close(c); +} + +static void sig_kotlin_params(sig_ctx_t *c, TSNode node) { + TSNode receiver = sig_field(node, "receiver"); + if (!ts_node_is_null(receiver)) { + /* An extension's receiver is part of its identity: String.f(Int) and + * Int.f(Int) share a base QN. */ + sig_entry_open(c); + sig_label(c, NULL, 0); + if (sig_writes_types(c)) { + sig_raw_str(c, "this:"); + sig_type_tokens(c, receiver); + } + sig_entry_close(c); + } + TSNode params = cbm_find_child_by_kind(node, "function_value_parameters"); + uint32_t count = ts_node_is_null(params) ? 0 : ts_node_named_child_count(params); + bool vararg = false; + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_named_child(params, i); + const char *k = ts_node_type(ch); + if (strcmp(k, "parameter_modifiers") == 0) { + vararg = vararg || sig_kotlin_is_vararg(c, ch); + } else if (strcmp(k, "parameter") == 0) { + sig_kotlin_param(c, ch, vararg); + vararg = false; + } + } +} + +/* ── Swift ─────────────────────────────────────────────────────── */ + +static void sig_swift_param(sig_ctx_t *c, TSNode p) { + TSNode external = sig_field(p, "external_name"); + TSNode internal = {0}; + uint32_t count = ts_node_child_count(p); + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_child(p, i); + const char *field = ts_node_field_name_for_child(p, i); + if (field && strcmp(field, "name") == 0 && + strcmp(ts_node_type(ch), "simple_identifier") == 0) { + internal = ch; + break; + } + } + sig_entry_open(c); + /* One name is both label and internal name; `_` means unlabeled. */ + sig_label_node(c, ts_node_is_null(external) ? internal : external); + if (sig_writes_types(c)) { + size_t before = c->len; + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_child(p, i); + const char *field = ts_node_field_name_for_child(p, i); + if (field && strcmp(field, "external_name") == 0) { + continue; + } + if (ts_node_eq(ch, internal)) { + continue; + } + const char *k = ts_node_type(ch); + if (strcmp(k, "parameter_modifiers") == 0) { + uint32_t mc = ts_node_named_child_count(ch); + for (uint32_t j = 0; j < mc; j++) { + TSNode m = ts_node_named_child(ch, j); + uint32_t ms = ts_node_start_byte(m); + if (ts_node_end_byte(m) > ms && c->src[ms] != '@') { + sig_type_tokens(c, m); /* inout; @escaping etc. dropped */ + } + } + } else if (ts_node_is_named(ch)) { + sig_type_tokens(c, ch); + } else if (sig_node_text_is(c, ch, "...")) { + sig_raw_str(c, sig_variadic_marker(c->lang)); + } + } + if (c->len == before) { + sig_raw(c, "?", 1); + } + } + sig_entry_close(c); +} + +static void sig_swift_params(sig_ctx_t *c, TSNode node) { + uint32_t count = ts_node_named_child_count(node); + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_named_child(node, i); + if (strcmp(ts_node_type(ch), "parameter") == 0) { + sig_swift_param(c, ch); + } + } +} + +/* ── Scala ─────────────────────────────────────────────────────── */ + +static void sig_scala_params(sig_ctx_t *c, TSNode node) { + /* Every parameter clause, flattened: all of them are erased into the + * method's JVM signature. */ + uint32_t count = ts_node_named_child_count(node); + for (uint32_t i = 0; i < count; i++) { + TSNode clause = ts_node_named_child(node, i); + if (strcmp(ts_node_type(clause), "parameters") != 0) { + continue; + } + uint32_t pc = ts_node_named_child_count(clause); + for (uint32_t j = 0; j < pc; j++) { + TSNode p = ts_node_named_child(clause, j); + if (strcmp(ts_node_type(p), "parameter") == 0) { + sig_entry_type(c, sig_field(p, "type"), NULL); + } + } + } +} + +/* ── Objective-C ───────────────────────────────────────────────── */ + +static void sig_objc_params(sig_ctx_t *c, TSNode node) { + /* Selector keywords are the method's direct `identifier` children: the + * first is the name, each later one labels the parameter after it. */ + uint32_t count = ts_node_child_count(node); + bool saw_name = false; + TSNode pending = {0}; + bool have_pending = false; + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_child(node, i); + const char *k = ts_node_type(ch); + if (strcmp(k, "identifier") == 0) { + if (saw_name) { + pending = ch; + have_pending = true; + } + saw_name = true; + } else if (strcmp(k, "method_parameter") == 0) { + sig_entry_open(c); + if (have_pending) { + sig_label_node(c, pending); + } else { + sig_label(c, NULL, 0); + } + have_pending = false; + if (sig_writes_types(c)) { + TSNode mt = cbm_find_child_by_kind(ch, "method_type"); + TSNode tn = ts_node_is_null(mt) ? mt : cbm_find_child_by_kind(mt, "type_name"); + size_t before = c->len; + sig_type_tokens(c, ts_node_is_null(tn) ? mt : tn); + if (c->len == before) { + sig_raw(c, "?", 1); + } + } + sig_entry_close(c); + } else if (!ts_node_is_named(ch) && sig_node_text_is(c, ch, "...")) { + sig_entry_open(c); + sig_raw_str(c, sig_variadic_marker(c->lang)); + sig_entry_close(c); + } + } +} + +/* ── Generic ───────────────────────────────────────────────────── */ + +static void sig_generic_params(sig_ctx_t *c, TSNode node) { + TSNode params = sig_field(node, "parameters"); + uint32_t count = ts_node_is_null(params) ? 0 : ts_node_named_child_count(params); + for (uint32_t i = 0; i < count; i++) { + TSNode p = ts_node_named_child(params, i); + if (sig_skipped_kind(ts_node_type(p))) { + continue; + } + TSNode type = sig_field(p, "type"); + sig_entry_type(c, type, NULL); + } +} + +/* ── Spelling pass ─────────────────────────────────────────────── */ + +/* Qualified type paths keep their last segment; "->" becomes "=>"; any other + * '.' or "::" (a receiver function type, a leading global scope) is dropped, + * so the suffix contract holds whatever the grammar produced. */ +static size_t sig_spell(char *s, size_t n) { + size_t out = 0; + size_t i = 0; + while (i < n) { + size_t sep = 0; + if (s[i] == '.') { + sep = 1; + } else if (s[i] == ':' && i + 1 < n && s[i + 1] == ':') { + sep = 2; + } + if (sep) { + bool ident_before = out > 0 && sig_ident_char((unsigned char)s[out - 1]); + bool ident_after = i + sep < n && sig_ident_char((unsigned char)s[i + sep]); + if (ident_before && ident_after) { + while (out > 0 && sig_ident_char((unsigned char)s[out - 1])) { + out--; + } + } + i += sep; + continue; + } + if (s[i] == '-' && i + 1 < n && s[i + 1] == '>') { + s[out++] = '='; + s[out++] = '>'; + i += 2; + continue; + } + s[out++] = s[i++]; + } + s[out] = '\0'; + return out; +} + +static uint64_t sig_fnv64(const char *s, size_t n) { + uint64_t h = SIG_FNV_OFFSET; + for (size_t i = 0; i < n; i++) { + h ^= (unsigned char)s[i]; + h *= SIG_FNV_PRIME; + } + return h; +} + +/* "(" leading whole entries ")" ... "#)" within CBM_CALLABLE_SIG_MAX. */ +static const char *sig_capped(CBMArena *a, const sig_ctx_t *c, const char *full, size_t n) { + char tail[SIG_CAPPED_TAIL + 1]; + snprintf(tail, sizeof(tail), "#%016llx)", (unsigned long long)sig_fnv64(full, n)); + size_t keep = 0; /* bytes of entry text after the "(" */ + bool comma = sig_writes_types(c); + int limit = c->too_many ? SIG_MAX_ENTRIES : c->entries; + for (int i = 0; i < limit; i++) { + size_t end = c->entry_end[i]; + if (end < c->open_off + 1 || end > c->close_off) { + break; + } + size_t candidate = end - (c->open_off + 1); + if (1 + candidate + (comma ? 1 : 0) + SIG_CAPPED_TAIL > CBM_CALLABLE_SIG_MAX) { + break; + } + keep = candidate; + } + char *out = cbm_arena_alloc(a, CBM_CALLABLE_SIG_MAX + 1); + if (!out) { + return NULL; + } + size_t len = 0; + out[len++] = '('; + memcpy(out + len, c->buf + c->open_off + 1, keep); + len += keep; + if (keep && comma) { + out[len++] = ','; + } + memcpy(out + len, tail, SIG_CAPPED_TAIL); + len += SIG_CAPPED_TAIL; + out[len] = '\0'; + return out; +} + +/* ── Builder ───────────────────────────────────────────────────── */ + +static void sig_render(sig_ctx_t *c, TSNode node) { + switch (c->lang) { + case CBM_LANG_CPP: + case CBM_LANG_CUDA: + case CBM_LANG_C: { + TSNode fdecl = sig_c_function_declarator(node); + if (c->lang != CBM_LANG_C && !sig_cpp_member_of_class_template(fdecl)) { + sig_cpp_tparams(c, node); + } + if (ts_node_is_null(fdecl)) { + c->open_off = c->len; + sig_raw(c, "()", 2); + c->close_off = c->len; + return; + } + sig_c_param_list(c, fdecl); + sig_cpp_cvref(c, fdecl); + return; + } + default: + break; + } + if (c->lang == CBM_LANG_CSHARP) { + sig_cs_tparams(c, node); + } + c->open_off = c->len; + sig_raw(c, "(", 1); + switch (c->lang) { + case CBM_LANG_JAVA: + sig_java_params(c, node); + break; + case CBM_LANG_CSHARP: + sig_cs_params(c, node); + break; + case CBM_LANG_KOTLIN: + sig_kotlin_params(c, node); + break; + case CBM_LANG_SWIFT: + sig_swift_params(c, node); + break; + case CBM_LANG_SCALA: + sig_scala_params(c, node); + break; + case CBM_LANG_OBJC: + sig_objc_params(c, node); + break; + default: + sig_generic_params(c, node); + break; + } + sig_raw(c, ")", 1); + c->close_off = c->len; +} + +static bool sig_roundtrips(const char *suffix, size_t n) { + /* "f" + suffix must invert to the base "f". */ + char probe[CBM_CALLABLE_SIG_MAX + 2]; + if (n > CBM_CALLABLE_SIG_MAX) { + return false; + } + probe[0] = 'f'; + memcpy(probe + 1, suffix, n); + probe[n + 1] = '\0'; + return cbm_qn_callable_base_len(probe) == 1; +} + +/* "(#)": the identity of a signature that cannot be spelled safely. */ +static const char *sig_hashed(CBMArena *a, const char *full, size_t n) { + return cbm_arena_sprintf(a, "(#%016llx)", (unsigned long long)sig_fnv64(full, n)); +} + +const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *source, + CBMLanguage lang, CBMCallableIdentity mode) { + if (!a || !source || ts_node_is_null(func_node) || mode == CBM_CALLABLE_ID_NONE) { + return NULL; + } + sig_ctx_t c; + memset(&c, 0, sizeof(c)); + c.arena = a; + c.src = source; + c.lang = lang; + c.mode = mode; + sig_raw(&c, "", 0); + if (c.overflow || !c.buf) { + return NULL; + } + c.buf[0] = '\0'; + sig_render(&c, func_node); + if (c.overflow) { + return NULL; + } + if (mode == CBM_CALLABLE_ID_ARITY) { + char digits[SIG_ARITY_DIGITS]; + snprintf(digits, sizeof(digits), "(%d)", c.entries); + return cbm_arena_strdup(a, digits); + } + size_t full = c.len; + char *spelled = cbm_arena_strndup(a, c.buf, full); + if (!spelled) { + return NULL; + } + size_t n = sig_spell(spelled, full); + if (n <= CBM_CALLABLE_SIG_MAX) { + return sig_roundtrips(spelled, n) ? spelled : sig_hashed(a, spelled, n); + } + /* Over the cap: re-derive the entry offsets in spelled coordinates. The + * spelling pass only shortens and only looks one byte ahead, and every + * recorded offset sits before a ',' or ')', so spelling a prefix gives + * exactly that prefix's spelled length. */ + int limit = c.too_many ? SIG_MAX_ENTRIES : c.entries; + for (int i = 0; i < limit; i++) { + char *prefix = cbm_arena_strndup(a, c.buf, c.entry_end[i]); + c.entry_end[i] = prefix ? sig_spell(prefix, c.entry_end[i]) : 0; + } + char *open_prefix = cbm_arena_strndup(a, c.buf, c.open_off); + char *close_prefix = cbm_arena_strndup(a, c.buf, c.close_off); + c.open_off = open_prefix ? sig_spell(open_prefix, c.open_off) : 0; + c.close_off = close_prefix ? sig_spell(close_prefix, c.close_off) : 0; + c.buf = spelled; + c.len = n; + const char *capped = sig_capped(a, &c, spelled, n); + if (capped && sig_roundtrips(capped, strlen(capped))) { + return capped; + } + return sig_hashed(a, spelled, n); +} + +const char *cbm_callable_sig(CBMArena *a, TSNode func_node, const char *source, CBMLanguage lang) { + return cbm_callable_sig_mode(a, func_node, source, lang, cbm_callable_identity(lang)); +} + +/* ── Inverse ───────────────────────────────────────────────────── */ + +static bool sig_ends_with_operator(const char *qn, size_t end) { + if (end < SIG_OPERATOR_LEN || + memcmp(qn + end - SIG_OPERATOR_LEN, "operator", SIG_OPERATOR_LEN) != 0) { + return false; + } + size_t at = end - SIG_OPERATOR_LEN; + return at == 0 || !sig_ident_char((unsigned char)qn[at - 1]); +} + +/* Whether qn[0..end) ends in a leaf a suffix may follow: an identifier (but + * not a make `$(VAR)` reference), the '>' closing template parameters, a C++ + * `operator...` leaf, or operator symbols — alone (Swift/Scala `+`, `==`) or + * after a Scala `_` (`unary_!`). A path such as `app/(auth)`, an anonymous + * `(anon)` leaf or an empty leaf is never a callable followed by a suffix. */ +static bool sig_callable_leaf_before(const char *qn, size_t end) { + size_t start = end; + while (start > 0 && qn[start - 1] != '.') { + start--; + } + if (start == end) { + return false; + } + unsigned char last = (unsigned char)qn[end - 1]; + if (last == '>' || (sig_ident_char(last) && last != '$')) { + return true; + } + if (end - start >= SIG_OPERATOR_LEN && memcmp(qn + start, "operator", SIG_OPERATOR_LEN) == 0) { + return true; + } + size_t op = end; + while (op > start && strchr(SIG_OPERATOR_CHARS, qn[op - 1]) != NULL) { + op--; + } + return op < end && (op == start || qn[op - 1] == '_'); +} + +size_t cbm_qn_callable_base_len_named(const char *qn, const char *name) { + size_t base = cbm_qn_callable_base_len(qn); + if (!qn || !name) { + return base; + } + size_t len = strlen(qn); + size_t n = strlen(name); + if (base == len || n == 0 || base < n || memcmp(qn + base - n, name, n) != 0) { + return len; + } + size_t at = base - n; + bool anchored = + at == 0 || qn[at - 1] == '.' || (at >= 2 && qn[at - 1] == ':' && qn[at - 2] == ':'); + return anchored ? base : len; +} + +size_t cbm_qn_callable_base_len(const char *qn) { + if (!qn) { + return 0; + } + size_t len = strlen(qn); + /* Trailing cvref (C++): const / volatile / & / && after the ')'. */ + size_t t = len; + for (;;) { + if (t > 0 && qn[t - 1] == '&') { + t--; + } else if (t >= SIG_CONST_LEN && + memcmp(qn + t - SIG_CONST_LEN, "const", SIG_CONST_LEN) == 0) { + t -= SIG_CONST_LEN; + } else if (t >= SIG_VOLATILE_LEN && + memcmp(qn + t - SIG_VOLATILE_LEN, "volatile", SIG_VOLATILE_LEN) == 0) { + t -= SIG_VOLATILE_LEN; + } else { + break; + } + } + if (t == 0 || qn[t - 1] != ')') { + return len; + } + /* The balanced "(...)" that ends at t. */ + int depth = 0; + size_t p = t; + bool found = false; + while (p > 0) { + p--; + if (qn[p] == ')') { + depth++; + } else if (qn[p] == '(') { + depth--; + if (depth == 0) { + found = true; + break; + } + } + } + if (!found || !sig_callable_leaf_before(qn, p)) { + return len; + } + /* A bare C++ call-operator leaf `...operator()` is a name, not a suffix. */ + if (t - p == 2 && sig_ends_with_operator(qn, p)) { + return len; + } + /* Optional "" right before the "(". */ + if (qn[p - 1] == '>') { + int adepth = 0; + size_t q = p; + while (q > 0) { + q--; + if (qn[q] == '>') { + adepth++; + } else if (qn[q] == '<') { + adepth--; + if (adepth == 0) { + break; + } + } else if (qn[q] == '.') { + q = 0; + adepth = 1; + break; + } + } + if (adepth == 0 && q > 0 && qn[q - 1] != '.' && !sig_ends_with_operator(qn, q)) { + return q; + } + } + return p; +} diff --git a/internal/cbm/callable_sig.h b/internal/cbm/callable_sig.h new file mode 100644 index 0000000000..c7b4d64fc9 --- /dev/null +++ b/internal/cbm/callable_sig.h @@ -0,0 +1,83 @@ +/* + * callable_sig.h — signature-qualified callable identity (#2061). + * + * A callable's graph identity is + * + * callable_qn := base_qn [] (params) [cvref] + * + * so overloads that share a base QN (Java/C#/C++/Kotlin/Swift/Scala/ObjC) + * become distinct nodes instead of merging into one node that collects every + * overload's CALLS edges. The `name` column stays the bare name. + * + * Every language selects a mode (cbm_callable_identity). A language whose + * mode is CBM_CALLABLE_ID_NONE keeps its historical QN byte for byte; the + * suffix is applied only where a mode is enabled, and the SAME builder + * (cbm_callable_sig) serves the definition QN and the call-scope QN so the two + * can never disagree. + * + * Suffix contract (what cbm_qn_callable_base_len inverts): + * - it never contains '.', "::" or "->" — the separators every QN leaf + * splitter uses — so a splitter that first strips the suffix finds the + * same leaf as before; + * - it is whitespace-normalized: one space only between two identifier + * characters ("unsigned long", "const T&"), none elsewhere; + * - type spelling: comments, annotations and attributes are dropped; + * qualified type paths keep their last segment (java.util.List -> List, + * std::vector -> vector); "->" in function types is spelled "=>"; a + * variadic/rest marker is "[]" for Java (the JVM array it is) and "~" + * everywhere else; C++ top-level cv-qualifiers are dropped and array + * parameters decay to pointers, as the language itself does; + * - it is at most CBM_CALLABLE_SIG_MAX bytes. A longer signature keeps the + * leading whole parameter entries that fit and ends in "#<16 hex>)", the + * FNV-1a 64 of the full uncapped suffix, so identity is preserved. + */ +#ifndef CBM_CALLABLE_SIG_H +#define CBM_CALLABLE_SIG_H + +#include "cbm.h" +#include "arena.h" +#include "tree_sitter/api.h" +#include + +typedef enum { + CBM_CALLABLE_ID_NONE = 0, /* historical base QN, no suffix */ + CBM_CALLABLE_ID_TYPED, /* (int,String) */ + CBM_CALLABLE_ID_LABELED_TYPED, /* (_:Data,to:URL) Swift */ + CBM_CALLABLE_ID_LABELED, /* (_:style:) ObjC selector */ + CBM_CALLABLE_ID_ARITY, /* (2) dynamic langs */ +} CBMCallableIdentity; + +enum { CBM_CALLABLE_SIG_MAX = 200 }; + +/* The identity mode a language uses for callable QNs. Every language is + * CBM_CALLABLE_ID_NONE until its enable change lands (with its index-format + * bump). Kept as a side table, like cbm_string_dispatch_suffixes, rather than + * a CBMLangSpec field: the ~160 positional lang_specs rows would all have to + * spell the new member under -Wmissing-field-initializers. */ +CBMCallableIdentity cbm_callable_identity(CBMLanguage lang); + +/* The identity suffix for the callable at `func_node` in the language's own + * mode, or NULL when that mode is NONE (or the node carries no parameter + * information the mode can use). Arena-owned. */ +const char *cbm_callable_sig(CBMArena *a, TSNode func_node, const char *source, CBMLanguage lang); + +/* Same, with an explicit mode — the builder the enable changes and the golden + * tables exercise before a language is switched on. */ +const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *source, + CBMLanguage lang, CBMCallableIdentity mode); + +/* Length of the base QN, i.e. `qn` without its callable identity suffix; equal + * to strlen(qn) when there is none. Purely syntactic (the inverse of the + * builder's contract above); a bare C++ `operator()` leaf is recognised as a + * name, not a suffix. NULL -> 0. */ +size_t cbm_qn_callable_base_len(const char *qn); + +/* The inverse anchored to the callable's bare `name` (the node's name + * column): the suffix is stripped only when the base QN is `name` or ends in + * "." / "::" + `name`. Other QN text can end in ')' as well — a Java field + * whose name is its declarator `X = f()`, a make `$(VAR)` target — and only + * the anchor makes the inverse exact on it. Use this form wherever the name + * is known; NULL name -> the unanchored form. */ +size_t cbm_qn_callable_base_len_named(const char *qn, const char *name); + +#endif /* CBM_CALLABLE_SIG_H */ diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index a00192a188..40532848de 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -238,6 +238,10 @@ typedef struct { * that declared this method. Kept at the tail so zero-initialised * callers in every other language remain ABI/source compatible. */ const char *impl_trait; + /* Callable identity (#2061): offset of the signature suffix inside + * qualified_name (base QN = the first qn_sig_off bytes); 0 = no suffix. + * Always 0 until a language enables its callable_identity mode. */ + uint32_t qn_sig_off; } CBMDefinition; /* Argument captured from a call expression */ @@ -422,6 +426,10 @@ typedef struct { uint32_t site_start_byte; // exact source occurrence; end > start when present uint32_t site_end_byte; // exclusive byte offset in the source file CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer + /* Callable identity (#2061): the resolved overload's signature suffix, + * appended to callee_qn when the edge is written; NULL = none (always, + * until a language enables its callable_identity mode). */ + const char *callee_sig; } CBMResolvedCall; typedef struct { diff --git a/internal/cbm/lsp/go_lsp.h b/internal/cbm/lsp/go_lsp.h index 65e97675c7..5874cbeefa 100644 --- a/internal/cbm/lsp/go_lsp.h +++ b/internal/cbm/lsp/go_lsp.h @@ -99,6 +99,9 @@ typedef struct { * a decorator rebinds the function name, so the undecorated definition is * not automatically an exact callable-value target. */ const char **decorators; + /* Callable identity (#2061): signature suffix of this def's graph QN; + * qualified_name stays the base QN. NULL = none (always, until enabled). */ + const char *sig; } CBMLSPDef; // 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 c752350b17..065ae25852 100644 --- a/internal/cbm/lsp/type_registry.h +++ b/internal/cbm/lsp/type_registry.h @@ -40,6 +40,9 @@ typedef struct { * remain NULL when raw cross-file provenance is ambiguous; the Rust trait * flag still prevents that method from being mistaken for inherent. */ const char *impl_trait_qn; + /* Callable identity (#2061): this overload's signature suffix; its graph + * QN is qualified_name + sig. NULL = none (always, until enabled). */ + const char *sig; } CBMRegisteredFunc; // Registered type with fields and method names. diff --git a/internal/cbm/result_compact.c b/internal/cbm/result_compact.c index d237b2a175..824852ae3b 100644 --- a/internal/cbm/result_compact.c +++ b/internal/cbm/result_compact.c @@ -355,6 +355,7 @@ static void cr_walk(cr_ctx_t *c, CBMFileResult *r) { cr_str(c, &r->resolved_calls.items[i].callee_qn); cr_str(c, &r->resolved_calls.items[i].strategy); cr_str(c, &r->resolved_calls.items[i].reason); + cr_str(c, &r->resolved_calls.items[i].callee_sig); } cr_array(c, (void **)&r->string_refs.items, r->string_refs.count, sizeof(CBMStringRef)); for (int i = 0; i < r->string_refs.count && r->string_refs.items; i++) { diff --git a/scripts/memory-core-baseline.txt b/scripts/memory-core-baseline.txt index ee54b498ba..9769cd9298 100644 --- a/scripts/memory-core-baseline.txt +++ b/scripts/memory-core-baseline.txt @@ -52,7 +52,7 @@ src/git/git_context.c 18 src/main.c 32 src/mcp/compact_out.c 39 src/mcp/index_supervisor.c 8 -src/mcp/mcp.c 784 +src/mcp/mcp.c 778 src/pipeline/artifact.c 38 src/pipeline/fqn.c 23 src/pipeline/lsp_resolve.h 4 diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 75ff8e396c..e1b0830e1b 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -58,6 +58,7 @@ enum { #include "cypher/cypher.h" #include "discover/discover.h" #include "pipeline/pipeline.h" +#include "callable_sig.h" /* cbm_qn_callable_base_len */ #include "pipeline/pass_cross_repo.h" #include "git/git_context.h" #include "cli/cli.h" @@ -4441,10 +4442,18 @@ static void sg_lines_str(char *out, size_t sz, int start, int end) { * ablation (tree > flat/DOT for LLM comprehension), Lost-in-Distance * (related rows adjacent — grouping by module does exactly that). */ -/* qn-prefix = qualified_name minus its last '.'-segment. Returns length. */ -static size_t sg_qn_prefix_len(const char *qn) { - const char *last = qn ? strrchr(qn, '.') : NULL; - return last ? (size_t)(last - qn) : 0; +/* qn-prefix = qualified_name minus its last '.'-segment. Returns length. + * The segment is found in the base QN: a callable identity suffix (#2061) + * stays on the row's short name, so prefix + "." + name still rebuilds it. */ +static size_t sg_qn_prefix_len(const char *qn, const char *name) { + size_t last = 0; + size_t base_len = cbm_qn_callable_base_len_named(qn, name); + for (size_t i = 0; i < base_len; i++) { + if (qn[i] == '.') { + last = i; + } + } + return last; } typedef struct { @@ -4711,7 +4720,7 @@ static void emit_search_results_grouped_tree(cbm_sb_t *sb, const cbm_search_outp const cbm_search_result_t *sr = &out->results[i]; const char *qn = sr->node.qualified_name ? sr->node.qualified_name : ""; const char *file = sr->node.file_path ? sr->node.file_path : ""; - size_t plen = sg_qn_prefix_len(qn); + size_t plen = sg_qn_prefix_len(qn, sr->node.name); bool same_group = previous_prefix && strlen(previous_prefix) == plen && memcmp(previous_prefix, qn, plen) == 0 && previous_file && strcmp(previous_file, file) == 0; @@ -4867,7 +4876,7 @@ static void emit_search_results_tree_json(yyjson_mut_doc *doc, yyjson_mut_val *r const cbm_search_result_t *sr = &out->results[i]; const char *qn = sr->node.qualified_name ? sr->node.qualified_name : ""; const char *file = sr->node.file_path ? sr->node.file_path : ""; - size_t plen = sg_qn_prefix_len(qn); + size_t plen = sg_qn_prefix_len(qn, sr->node.name); bool same_group = previous_prefix && strlen(previous_prefix) == plen && memcmp(previous_prefix, qn, plen) == 0 && previous_file && strcmp(previous_file, file) == 0; @@ -8756,7 +8765,7 @@ static yyjson_mut_val *bfs_to_tree_json(yyjson_mut_doc *doc, cbm_traverse_result } const char *qn = tr->visited[i].node.qualified_name ? tr->visited[i].node.qualified_name : ""; - size_t plen = sg_qn_prefix_len(qn); + size_t plen = sg_qn_prefix_len(qn, tr->visited[i].node.name); if (!have_group || strlen(cur_group) != plen || memcmp(cur_group, qn, plen) != 0) { char *next_group = cbm_strndup(qn, plen); if (!next_group) { @@ -8880,7 +8889,7 @@ static void bfs_to_tree_table(cbm_sb_t *sb, const char *key, cbm_traverse_result char *cur_group = NULL; for (int i = 0; i < ordered_count; i++) { const char *qn = ordered[i].node.qualified_name ? ordered[i].node.qualified_name : ""; - size_t plen = sg_qn_prefix_len(qn); + size_t plen = sg_qn_prefix_len(qn, ordered[i].node.name); if (!cur_group || strlen(cur_group) != plen || memcmp(cur_group, qn, plen) != 0) { char *next_group = cbm_strndup(qn, plen); if (!next_group) { @@ -9161,6 +9170,13 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) { } } } + if (node_count == 0) { + /* A bare base QN names every signature-qualified overload (#2061); + * several overloads report ambiguity below like same-named nodes. */ + cbm_store_free_nodes(nodes, 0); + nodes = NULL; + cbm_store_find_nodes_by_qn_base(store, project, func_name, false, &nodes, &node_count); + } if (node_count == 0) { yyjson_mut_doc *error_doc = yyjson_mut_doc_new(NULL); @@ -12392,6 +12408,34 @@ static char *handle_get_file_outline(cbm_mcp_server_t *srv, const char *args) { return result; } +/* Respond from one lookup tier's candidates (always consumed), or NULL when + * the tier found nothing. One match answers directly; several prefer the real + * definition (a .c body over a .h declaration, a Function over a Module) so + * an unambiguous-by-preference match resolves without a disambiguation round + * trip; only a genuine tie returns suggestions. */ +static char *snippet_from_tier(cbm_mcp_server_t *srv, cbm_node_t *nodes, int count, + const char *input, const char *match_method, bool include_neighbors, + const char *args) { + if (count <= 0) { + cbm_store_free_nodes(nodes, count); + return NULL; + } + bool ambiguous = false; + int sel = count == SKIP_ONE ? 0 : pick_resolved_node(nodes, count, &ambiguous); + if (ambiguous) { + char *result = snippet_suggestions(input, nodes, count); + cbm_store_free_nodes(nodes, count); + return result; + } + cbm_node_t node = {0}; + copy_node(&nodes[sel], &node); + cbm_store_free_nodes(nodes, count); + char *result = + build_snippet_response(srv, &node, match_method, include_neighbors, NULL, 0, args); + free_node_contents(&node); + return result; +} + static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) { char *qn = cbm_mcp_get_string_arg(args, "qualified_name"); char *project = get_project_arg(args); @@ -12433,50 +12477,38 @@ static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) { return result; } + /* Tier 1b: a bare base QN names every signature-qualified overload of it + * (#2061). Finds nothing for QNs without a callable identity suffix. */ + cbm_node_t *tier_nodes = NULL; + int tier_count = 0; + cbm_store_find_nodes_by_qn_base(store, effective_project, qn, false, &tier_nodes, &tier_count); + char *result = + snippet_from_tier(srv, tier_nodes, tier_count, qn, "base", include_neighbors, args); + /* Tier 2: Suffix match — handles partial QNs ("main.HandleRequest") * and short names ("ProcessOrder") via LIKE '%.X'. */ - cbm_node_t *suffix_nodes = NULL; - int suffix_count = 0; - cbm_store_find_nodes_by_qn_suffix(store, effective_project, qn, &suffix_nodes, &suffix_count); - - if (suffix_count == SKIP_ONE) { - copy_node(&suffix_nodes[0], &node); - cbm_store_free_nodes(suffix_nodes, suffix_count); - char *result = - build_snippet_response(srv, &node, "suffix", include_neighbors, NULL, 0, args); - free_node_contents(&node); - free(qn); - free(project); - return result; - } - - if (suffix_count > SKIP_ONE) { - /* Prefer the real definition (a .c body over a .h declaration, a Function - * over a Module) so an unambiguous-by-preference match resolves directly - * instead of forcing a disambiguation round trip; only a genuine tie still - * returns suggestions. */ - bool snip_ambiguous = false; - int ssel = pick_resolved_node(suffix_nodes, suffix_count, &snip_ambiguous); - if (!snip_ambiguous) { - copy_node(&suffix_nodes[ssel], &node); - cbm_store_free_nodes(suffix_nodes, suffix_count); - char *result = - build_snippet_response(srv, &node, "suffix", include_neighbors, NULL, 0, args); - free_node_contents(&node); - free(qn); - free(project); - return result; - } - char *result = snippet_suggestions(qn, suffix_nodes, suffix_count); - cbm_store_free_nodes(suffix_nodes, suffix_count); - free(qn); - free(project); - return result; + if (!result) { + tier_nodes = NULL; + tier_count = 0; + cbm_store_find_nodes_by_qn_suffix(store, effective_project, qn, &tier_nodes, &tier_count); + result = + snippet_from_tier(srv, tier_nodes, tier_count, qn, "suffix", include_neighbors, args); + } + + /* Tier 2b: the partial/short form of Tier 1b ("Session.upload"). */ + if (!result) { + tier_nodes = NULL; + tier_count = 0; + cbm_store_find_nodes_by_qn_base(store, effective_project, qn, true, &tier_nodes, + &tier_count); + result = + snippet_from_tier(srv, tier_nodes, tier_count, qn, "suffix", include_neighbors, args); } - - cbm_store_free_nodes(suffix_nodes, suffix_count); free(qn); free(project); + if (result) { + return result; + } /* Nothing found — guide the caller toward search_graph */ return cbm_mcp_text_result( diff --git a/src/pipeline/lsp_resolve.h b/src/pipeline/lsp_resolve.h index 96f08e84e0..c3a6a419c4 100644 --- a/src/pipeline/lsp_resolve.h +++ b/src/pipeline/lsp_resolve.h @@ -22,6 +22,7 @@ #define CBM_PIPELINE_LSP_RESOLVE_H #include "cbm.h" +#include "callable_sig.h" /* cbm_qn_callable_base_len — leaf splitters skip the suffix */ #include "graph_buffer/graph_buffer.h" #include "foundation/constants.h" @@ -64,7 +65,10 @@ static inline const char *cbm_lsp_bare_segment(const char *name) { return name; } const char *seg = name; - for (const char *p = name; *p; p++) { + /* Scan the base only: a callable identity suffix (#2061) belongs to the + * leaf and may itself contain ':' (Swift labels) or '>' (generics). */ + const char *end = name + cbm_qn_callable_base_len(name); + for (const char *p = name; p < end; p++) { /* '.' (dotted QN / Java-style member) and ':' (C++ `::`, last colon * wins) are member/scope separators. '>' is only a separator when it * closes the `->` arrow (preceded by '-'); a bare '>' closes a template @@ -86,7 +90,15 @@ static inline const char *cbm_pipeline_qn_class_method_tail(const char *qn) { if (!qn) { return NULL; } - const char *last = strrchr(qn, '.'); + /* The last '.' of the BASE QN: a callable identity suffix (#2061) never + * contains '.', so this is the historical strrchr for every QN. */ + const char *last = NULL; + size_t base_len = cbm_qn_callable_base_len(qn); + for (size_t i = 0; i < base_len; i++) { + if (qn[i] == '.') { + last = qn + i; + } + } if (!last || last == qn) { return NULL; } diff --git a/src/pipeline/pass_ensemble_routing.c b/src/pipeline/pass_ensemble_routing.c index 0aa574839d..1a6ca09809 100644 --- a/src/pipeline/pass_ensemble_routing.c +++ b/src/pipeline/pass_ensemble_routing.c @@ -6,6 +6,7 @@ #include "foundation/compat_fs.h" #include "foundation/constants.h" #include "foundation/str_util.h" +#include "callable_sig.h" #include #include @@ -425,7 +426,14 @@ static void collect_prod_defs(cbm_pipeline_ctx_t *ctx, ens_prod_def_t ***defs_ou char class_qn[CBM_SZ_256]; class_qn[0] = '\0'; if (xd->qualified_name) { - const char *dot = strrchr(xd->qualified_name, '.'); + /* Owner = base QN minus its leaf (#2061 suffixes hold no '.'). */ + const char *dot = NULL; + size_t base_len = cbm_qn_callable_base_len_named(xd->qualified_name, xd->name); + for (size_t i = 0; i < base_len; i++) { + if (xd->qualified_name[i] == '.') { + dot = xd->qualified_name + i; + } + } if (dot) { int len = (int)(dot - xd->qualified_name); if (len > 0 && len < CBM_SZ_256) { diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 3b74cc1ebb..d484d18c25 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -19,6 +19,7 @@ #include "result_spill.h" #include "pipeline/pipeline_internal.h" #include "pipeline/lsp_resolve.h" +#include "callable_sig.h" #include "lsp/go_lsp.h" #include "lsp/c_lsp.h" #include "lsp/py_lsp.h" @@ -273,12 +274,20 @@ static const char *pxc_join_base_qns(CBMArena *arena, const char *const *bases, static bool pxc_is_jvm_lang(CBMLanguage lang); +/* Leaf of the BASE QN: a callable identity suffix (#2061) never contains + * '.', so this equals the historical strrchr split for every QN. */ static const char *pxc_last_component(const char *qn) { if (!qn) { return NULL; } - const char *dot = strrchr(qn, '.'); - return dot ? dot + 1 : qn; + const char *leaf = qn; + size_t base_len = cbm_qn_callable_base_len(qn); + for (size_t i = 0; i < base_len; i++) { + if (qn[i] == '.') { + leaf = qn + i + 1; + } + } + return leaf; } /* Every return is arena-owned: the fallback spelling is a copy, never the @@ -377,7 +386,9 @@ static const char *pxc_qn_leaf(const char *name) { return NULL; } const char *leaf = name; - for (const char *p = name; *p; p++) { + /* Base only: a callable identity suffix (#2061) may carry ':' labels. */ + const char *end = name + cbm_qn_callable_base_len(name); + for (const char *p = name; p < end; p++) { if (*p == '.' || *p == ':' || *p == '/' || *p == '\\') { leaf = p + 1; } diff --git a/src/pipeline/pass_semantic.c b/src/pipeline/pass_semantic.c index 48dab00831..597d629ba4 100644 --- a/src/pipeline/pass_semantic.c +++ b/src/pipeline/pass_semantic.c @@ -23,6 +23,7 @@ #include "foundation/compat_fs.h" #include "foundation/limits.h" #include "cbm.h" +#include "callable_sig.h" #include #include @@ -356,7 +357,14 @@ static void go_index_add(go_method_index_t *ix, const char *name, int type_index static void go_index_visit_node(const cbm_gbuf_node_t *node, void *userdata) { go_method_index_t *ix = userdata; const char *qn = node->qualified_name; - const char *dot = qn ? strrchr(qn, '.') : NULL; + /* Last '.' of the base QN (a callable identity suffix never holds one). */ + const char *dot = NULL; + size_t base_len = cbm_qn_callable_base_len_named(qn, node->name); + for (size_t i = 0; qn && i < base_len; i++) { + if (qn[i] == '.') { + dot = qn + i; + } + } if (!dot || dot == qn || !dot[SKIP_ONE]) { return; } diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index c99c72d8d5..2d0a79a12b 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -30,6 +30,7 @@ enum { REG_MAX_CANDIDATES = 256 }; #include "foundation/hash_table.h" #include "foundation/dyn_array.h" #include "foundation/platform.h" +#include "callable_sig.h" /* cbm_qn_callable_base_len_named: overloads share the name key */ #include #include @@ -860,7 +861,6 @@ void cbm_registry_free(cbm_registry_t *r) { void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, const char *label) { - (void)name; if (!r || !qualified_name || !label) { return; } @@ -893,9 +893,13 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified cbm_ht_set(r->exact, strdup(qualified_name), (void *)interned); const char *owned_qn = cbm_ht_get_key(r->exact, qualified_name); - /* Index by simple name. + /* Index by simple name. A signature-qualified callable (#2061) is indexed + * by its bare `name`, so every overload shares one bucket; any other QN + * keeps its historical last-segment key. * No array dedup needed: exact-map check above guarantees uniqueness. */ - const char *simple = simple_name(qualified_name); + const char *simple = name && cbm_qn_callable_base_len_named(owned_qn, name) < strlen(owned_qn) + ? name + : simple_name(qualified_name); qn_array_t *arr = cbm_ht_get(r->by_name, simple); if (!arr) { arr = calloc(CBM_ALLOC_ONE, sizeof(qn_array_t)); diff --git a/src/store/store.c b/src/store/store.c index fa074cb5db..f4a2ab378d 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -78,6 +78,8 @@ enum { #include "foundation/compat.h" #include "foundation/log.h" #include "foundation/compat_regex.h" +#include "callable_sig.h" /* cbm_qn_callable_base_len: base-match tier */ +#include "foundation/mem_core.h" /* cbm_alloc: pattern buffers */ #include "foundation/str_util.h" #define XXH_INLINE_ALL @@ -4376,29 +4378,95 @@ int cbm_store_find_nodes_by_file_overlap(cbm_store_t *s, const char *project, co /* ── FindNodesByQNSuffix ───────────────────────────────────────── */ +/* Row filter on the raw qualified_name + name columns (NULL keeps every row). */ +typedef bool (*store_qn_keep_fn)(const char *qn, const char *name, const void *arg); + +/* Step `stmt` (columns as scan_node reads them) into a node array, keeping the + * rows `keep` accepts. Finalizes `stmt`. */ +static int store_collect_nodes(cbm_store_t *s, sqlite3_stmt *stmt, store_qn_keep_fn keep, + const void *keep_arg, cbm_node_t **out, int *count) { + int cap = ST_INIT_CAP_8; + int n = 0; + cbm_node_t *nodes = malloc(cap * sizeof(cbm_node_t)); + int scan_rc8; + while ((scan_rc8 = sqlite3_step(stmt)) == SQLITE_ROW) { + if (keep && !keep((const char *)sqlite3_column_text(stmt, CBM_SZ_4), + (const char *)sqlite3_column_text(stmt, CBM_SZ_3), keep_arg)) { + continue; + } + if (n >= cap) { + cap *= ST_GROWTH; + nodes = safe_realloc(nodes, cap * sizeof(cbm_node_t)); + } + memset(&nodes[n], 0, sizeof(cbm_node_t)); + scan_node(stmt, &nodes[n]); + n++; + } + if (scan_rc8 != SQLITE_DONE) { /* SCANCHK:8:stmt */ + store_set_error_sqlite(s, "row scan aborted"); + sqlite3_finalize(stmt); + cbm_store_free_nodes(nodes, n); + *out = NULL; + *count = 0; + return CBM_STORE_ERR; + } + sqlite3_finalize(stmt); + *out = nodes; + *count = n; + return CBM_STORE_OK; +} + +/* "%." + text + tail with LIKE's wildcards ('%', '_') and the escape + * character escaped, for `LIKE ? ESCAPE '\'`. Heap-owned. The caller's text + * is matched literally at any length: a fixed 512-byte buffer used to + * truncate a long suffix into a pattern that matched nothing, and an + * unescaped '_' in `my_func` also matched `myXfunc`. */ +static char *store_like_dot_suffix(const char *text, const char *tail) { + size_t n = strlen(text); + size_t tail_len = strlen(tail); + char *pat = cbm_alloc(CBM_MEM_CLASS_STORE, (2 * n) + tail_len + ST_COL_3); /* "%." + NUL */ + if (!pat) { + return NULL; + } + size_t k = 0; + pat[k++] = '%'; + pat[k++] = '.'; + for (size_t i = 0; i < n; i++) { + if (text[i] == '%' || text[i] == '_' || text[i] == '\\') { + pat[k++] = '\\'; + } + pat[k++] = text[i]; + } + memcpy(pat + k, tail, tail_len + SKIP_ONE); + return pat; +} + int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const char *suffix, cbm_node_t **out, int *count) { *out = NULL; *count = 0; - if (!s || !s->db) { + if (!s || !s->db || !suffix) { return CBM_STORE_ERR; } /* Match QNs ending with ".suffix" or exactly equal to suffix */ - char like_pattern[CBM_SZ_512]; - snprintf(like_pattern, sizeof(like_pattern), "%%.%s", suffix); + char *like_pattern = store_like_dot_suffix(suffix, ""); + if (!like_pattern) { + return CBM_STORE_ERR; + } const char *sql_with_project = "SELECT id, project, label, name, qualified_name, file_path, " "start_line, end_line, properties FROM nodes " - "WHERE project = ?1 AND (qualified_name LIKE ?2 OR qualified_name = ?3)"; + "WHERE project = ?1 AND (qualified_name LIKE ?2 ESCAPE '\\' OR qualified_name = ?3)"; const char *sql_any = "SELECT id, project, label, name, qualified_name, file_path, " "start_line, end_line, properties FROM nodes " - "WHERE (qualified_name LIKE ?1 OR qualified_name = ?2)"; + "WHERE (qualified_name LIKE ?1 ESCAPE '\\' OR qualified_name = ?2)"; sqlite3_stmt *stmt = NULL; int rc = sqlite3_prepare_v2(s->db, project ? sql_with_project : sql_any, CBM_NOT_FOUND, &stmt, NULL); if (rc != SQLITE_OK) { + cbm_free(CBM_MEM_CLASS_STORE, like_pattern); store_set_error_sqlite(s, "qn_suffix prepare"); return CBM_STORE_ERR; } @@ -4411,32 +4479,130 @@ int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const bind_text(stmt, SKIP_ONE, like_pattern); bind_text(stmt, ST_COL_2, suffix); } + rc = store_collect_nodes(s, stmt, NULL, NULL, out, count); + cbm_free(CBM_MEM_CLASS_STORE, like_pattern); + return rc; +} - int cap = ST_INIT_CAP_8; - int n = 0; - cbm_node_t *nodes = malloc(cap * sizeof(cbm_node_t)); - int scan_rc8; - while ((scan_rc8 = sqlite3_step(stmt)) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - nodes = safe_realloc(nodes, cap * sizeof(cbm_node_t)); +/* ── FindNodesByQNBase ─────────────────────────────────────────── */ + +typedef struct { + const char *base; + size_t len; + bool suffix; +} store_qn_base_arg_t; + +/* ASCII case-insensitive equality: SQLite's LIKE folds exactly ASCII. */ +static bool store_ascii_ieq(const char *a, const char *b, size_t n) { + for (size_t i = 0; i < n; i++) { + unsigned char x = (unsigned char)a[i]; + unsigned char y = (unsigned char)b[i]; + if (x >= 'A' && x <= 'Z') { + x = (unsigned char)(x - 'A' + 'a'); + } + if (y >= 'A' && y <= 'Z') { + y = (unsigned char)(y - 'A' + 'a'); + } + if (x != y) { + return false; } - memset(&nodes[n], 0, sizeof(cbm_node_t)); - scan_node(stmt, &nodes[n]); - n++; } - if (scan_rc8 != SQLITE_DONE) { /* SCANCHK:8:stmt */ - store_set_error_sqlite(s, "row scan aborted"); - sqlite3_finalize(stmt); - cbm_store_free_nodes(nodes, n); - *out = NULL; - *count = 0; + return true; +} + +/* Keep a row whose QN carries a callable identity suffix (#2061) over `base`: + * its base QN equals `base` (exact) or ends with "." + `base` (suffix mode, + * ASCII case-insensitive like the LIKE suffix tier). An unsuffixed QN never + * qualifies, so this tier finds nothing until a language mints suffixes. */ +static bool store_qn_base_keep(const char *qn, const char *name, const void *argp) { + const store_qn_base_arg_t *arg = argp; + if (!qn || !name) { + return false; + } + size_t base_len = cbm_qn_callable_base_len_named(qn, name); + if (qn[base_len] == '\0') { + return false; + } + if (!arg->suffix) { + return base_len == arg->len && memcmp(qn, arg->base, arg->len) == 0; + } + return base_len > arg->len && qn[base_len - arg->len - SKIP_ONE] == '.' && + store_ascii_ieq(qn + base_len - arg->len, arg->base, arg->len); +} + +/* Exact mode reads the qualified_name index range [base "(", base "="): a + * suffix starts with '(' (0x28) or '<' (0x3C). Suffix mode needs the leading + * wildcard, like the suffix tier it follows. Both bounds are heap-owned. */ +static bool store_qn_base_bounds(const char *base, bool suffix_match, char **lo, char **hi) { + if (suffix_match) { + *lo = store_like_dot_suffix(base, "(%"); + *hi = store_like_dot_suffix(base, "<%"); + } else { + size_t n = strlen(base); + *lo = cbm_alloc(CBM_MEM_CLASS_STORE, n + ST_COL_2); + *hi = cbm_alloc(CBM_MEM_CLASS_STORE, n + ST_COL_2); + if (*lo && *hi) { + memcpy(*lo, base, n); + memcpy(*hi, base, n); + (*lo)[n] = '('; + (*hi)[n] = '='; + (*lo)[n + SKIP_ONE] = '\0'; + (*hi)[n + SKIP_ONE] = '\0'; + } + } + if (*lo && *hi) { + return true; + } + cbm_free(CBM_MEM_CLASS_STORE, *lo); + cbm_free(CBM_MEM_CLASS_STORE, *hi); + return false; +} + +int cbm_store_find_nodes_by_qn_base(cbm_store_t *s, const char *project, const char *base, + bool suffix_match, cbm_node_t **out, int *count) { + *out = NULL; + *count = 0; + if (!s || !s->db || !base || !base[0]) { + return CBM_STORE_ERR; + } + static const char *const sql[2][2] = { + /* [suffix_match][has project] */ + {"SELECT id, project, label, name, qualified_name, file_path, start_line, end_line, " + "properties FROM nodes WHERE qualified_name >= ?1 AND qualified_name < ?2", + "SELECT id, project, label, name, qualified_name, file_path, start_line, end_line, " + "properties FROM nodes WHERE project = ?1 AND qualified_name >= ?2 AND " + "qualified_name < ?3"}, + {"SELECT id, project, label, name, qualified_name, file_path, start_line, end_line, " + "properties FROM nodes WHERE (qualified_name LIKE ?1 ESCAPE '\\' OR " + "qualified_name LIKE ?2 ESCAPE '\\')", + "SELECT id, project, label, name, qualified_name, file_path, start_line, end_line, " + "properties FROM nodes WHERE project = ?1 AND (qualified_name LIKE ?2 ESCAPE '\\' OR " + "qualified_name LIKE ?3 ESCAPE '\\')"}, + }; + char *lo = NULL; + char *hi = NULL; + if (!store_qn_base_bounds(base, suffix_match, &lo, &hi)) { return CBM_STORE_ERR; } - sqlite3_finalize(stmt); - *out = nodes; - *count = n; - return CBM_STORE_OK; + sqlite3_stmt *stmt = NULL; + if (sqlite3_prepare_v2(s->db, sql[suffix_match ? 1 : 0][project ? 1 : 0], CBM_NOT_FOUND, &stmt, + NULL) != SQLITE_OK) { + cbm_free(CBM_MEM_CLASS_STORE, lo); + cbm_free(CBM_MEM_CLASS_STORE, hi); + store_set_error_sqlite(s, "qn_base prepare"); + return CBM_STORE_ERR; + } + int col = SKIP_ONE; + if (project) { + bind_text(stmt, col++, project); + } + bind_text(stmt, col++, lo); + bind_text(stmt, col, hi); + store_qn_base_arg_t arg = {base, strlen(base), suffix_match}; + int rc = store_collect_nodes(s, stmt, store_qn_base_keep, &arg, out, count); + cbm_free(CBM_MEM_CLASS_STORE, lo); + cbm_free(CBM_MEM_CLASS_STORE, hi); + return rc; } /* ── NodeDegree ────────────────────────────────────────────────── */ diff --git a/src/store/store.h b/src/store/store.h index b6ce844f75..3e387222de 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -105,6 +105,13 @@ int cbm_store_find_nodes_by_file_overlap(cbm_store_t *s, const char *project, co int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const char *suffix, cbm_node_t **out, int *count); +/* Find callables whose signature-qualified QN (#2061) has `base` as its base + * QN (exact), or a base ending with "." + `base` (suffix_match). QNs without + * a callable identity suffix never match: this is the tier that lets a bare + * QN name every overload. */ +int cbm_store_find_nodes_by_qn_base(cbm_store_t *s, const char *project, const char *base, + bool suffix_match, cbm_node_t **out, int *count); + /* Get CALLS degree of a node (inbound and outbound). */ void cbm_store_node_degree(cbm_store_t *s, int64_t node_id, int *in_deg, int *out_deg); diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c new file mode 100644 index 0000000000..ad0315032a --- /dev/null +++ b/tests/test_callable_sig.c @@ -0,0 +1,348 @@ +/* + * test_callable_sig.c — golden tables for the signature-qualified callable + * identity builder and its inverse (#2061, plumbing: no language enabled). + */ +#include "test_framework.h" +#include "callable_sig.h" +#include "lang_specs.h" +#include "foundation/arena.h" +#include "pipeline/pipeline.h" +#include "pipeline/lsp_resolve.h" +#include "tree_sitter/api.h" +#include + +typedef struct { + CBMLanguage lang; + CBMCallableIdentity mode; + const char *source; + const char *kind; /* node kind of the callable */ + int nth; /* 0-based occurrence of `kind` in preorder */ + const char *expected; +} sig_case_t; + +static bool find_nth(TSNode n, const char *kind, int *left, TSNode *out) { + if (strcmp(ts_node_type(n), kind) == 0) { + if (*left == 0) { + *out = n; + return true; + } + (*left)--; + } + uint32_t count = ts_node_child_count(n); + for (uint32_t i = 0; i < count; i++) { + if (find_nth(ts_node_child(n, i), kind, left, out)) { + return true; + } + } + return false; +} + +/* Parse `source`, build the nth `kind` node's suffix in `mode`; the returned + * string is arena-owned (NULL when absent). */ +static const char *sig_of(CBMArena *a, CBMLanguage lang, CBMCallableIdentity mode, + const char *source, const char *kind, int nth) { + TSParser *parser = ts_parser_new(); + ts_parser_set_language(parser, cbm_ts_language(lang)); + TSTree *tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)strlen(source)); + TSNode node = {0}; + int left = nth; + const char *sig = NULL; + if (tree && find_nth(ts_tree_root_node(tree), kind, &left, &node)) { + sig = cbm_callable_sig_mode(a, node, source, lang, mode); + } + ts_tree_delete(tree); + ts_parser_delete(parser); + return sig; +} + +static const sig_case_t k_cases[] = { + /* Java: generics kept, qualifiers and annotations dropped, varargs = []. */ + {CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, + "class A { > void work(final java.util.List xs, int[] a, " + "String... rest) {} }", + "method_declaration", 0, "(List,int[],String[])"}, + {CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, "class A { void f(@Ann Map m, int a[]){} }", + "method_declaration", 0, "(Map,int[])"}, + {CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, + "class A { void f(\n @Ann Map< K ,\n V > m, /* c */ int a [ ]\n) {} }", "method_declaration", + 0, "(Map,int[])"}, + {CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, "class A { A(int x){} void g(){} }", + "constructor_declaration", 0, "(int)"}, + {CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, "class A { A(int x){} void g(){} }", + "method_declaration", 0, "()"}, + /* C#: generic arity, ref/out, params inlined by the grammar, tuple names + * dropped, operators and indexers. */ + {CBM_LANG_CSHARP, CBM_CALLABLE_ID_TYPED, + "class A { public void Work(ref int x, out string y, params object[] rest, int? n = 3, " + "List> m, (int a, string b) t, this Foo z) {} }", + "method_declaration", 0, + "(ref int,out string,object[],int?,List>,(int,string),Foo)"}, + {CBM_LANG_CSHARP, CBM_CALLABLE_ID_TYPED, + "class A { public static A operator +(A a, A b) => a; }", "operator_declaration", 0, "(A,A)"}, + {CBM_LANG_CSHARP, CBM_CALLABLE_ID_TYPED, "class A { int this[int i] { get => 0; } }", + "indexer_declaration", 0, "(int)"}, + /* C++: template kinds, declarator markers, top-level cv dropped, arrays + * decay, function pointers, C variadic, packs, cvref, operators. */ + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, + "struct S { template void work(const std::vector & v, " + "int (*cb)(int), unsigned long x, ...) const && noexcept; };", + "template_declaration", 0, + "(const vector&,int(*)(int),unsigned long,~)const&&"}, + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "struct S { S& operator=(S&&) &; };", "field_declaration", + 0, "(S&&)&"}, + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "struct S { bool operator()(int) volatile; };", + "field_declaration", 0, "(int)volatile"}, + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, + "struct S { template void emplace(Args&&... args); };", "template_declaration", + 0, "(Args&&~)"}, + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "void n::S::arr(int a[10], char *const p, int q) {}", + "function_definition", 0, "(int*,char*,int)"}, + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, + "void w(\n const int a , // trailing\n char const * /* c */ const p\n);", + "declaration", 0, "(int,const char*)"}, + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "void v(void);", "declaration", 0, "()"}, + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, "void f(int (*(*g)(int x))(double), void h(char *s));", + "declaration", 0, "(int(*(*)(int))(double),void(*)(char*))"}, + {CBM_LANG_CPP, CBM_CALLABLE_ID_TYPED, + "template struct S { void f(int); };\ntemplate void S::f(int x) {}", + "function_definition", 0, "(int)"}, + /* Kotlin: extension receiver, vararg, function types, nullable, + * qualified types, suspend. */ + {CBM_LANG_KOTLIN, CBM_CALLABLE_ID_TYPED, + "class A {\n fun > String.work(vararg xs: Int, f: (Int) -> Unit, " + "m: Map>? = null, @Ann q: kotlin.collections.List<*>) {}\n}", + "function_declaration", 0, "(this:String,Int~,(Int)=>Unit,Map>?,List<*>)"}, + {CBM_LANG_KOTLIN, CBM_CALLABLE_ID_TYPED, "fun top(x: Int) = x", "function_declaration", 0, + "(Int)"}, + {CBM_LANG_KOTLIN, CBM_CALLABLE_ID_TYPED, "class A { constructor(x: Int) {} }", + "secondary_constructor", 0, "(Int)"}, + /* Swift: labels + types (the #2061 Alamofire shape), variadic, inout, + * attributes dropped, qualified types, init/subscript/operators. */ + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "class Session {\n func upload(_ data: Data, to url: URLConvertible, method: " + "HTTPMethod = .post, headers: [String: String]? = nil, handler: @escaping (Int) -> Void, " + "xs: Int..., io: inout Swift.Int, t: (a: Int, String)) -> UploadRequest { }\n}", + "function_declaration", 0, + "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(Int)=>Void," + "xs:Int~,io:inout Int,t:(a:Int,String))"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "class Session {\n func upload(_ data: Data, with request: URLRequest) {}\n}", + "function_declaration", 0, "(_:Data,with:URLRequest)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class C { init(frame: CGRect) {} }", + "init_declaration", 0, "(frame:CGRect)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "class C { subscript(index i: Int) -> Int { return 0 } }", "subscript_declaration", 0, + "(index:Int)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, "(l:C,r:C)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func extra() {}", "function_declaration", 0, + "()"}, + /* Scala: every clause flattened, by-name and repeated parameters. */ + {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, + "class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: " + "Ordering[T]): Unit = {}\n}", + "function_definition", 0, "(List[T],=>Int,String*,Ordering[T])"}, + {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, + "class A {\n def f(g: (Int, String) => scala.collection.immutable.Map[String, Int]): Unit\n}", + "function_declaration", 0, "((Int,String)=>Map[String,Int])"}, + /* Objective-C: the selector's keyword labels. */ + {CBM_LANG_OBJC, CBM_CALLABLE_ID_LABELED, + "@implementation Foo\n- (instancetype)initWithFrame:(CGRect)frame style:(NSInteger)style " + "{ return self; }\n@end", + "method_definition", 0, "(_:style:)"}, + {CBM_LANG_OBJC, CBM_CALLABLE_ID_LABELED, "@implementation Foo\n- (void)run { }\n@end", + "method_definition", 0, "()"}, + {CBM_LANG_OBJC, CBM_CALLABLE_ID_LABELED, + "@implementation Foo\n- (void)setX:(int)x :(int)y {}\n@end", "method_definition", 0, "(_:_:)"}, + {CBM_LANG_OBJC, CBM_CALLABLE_ID_LABELED, + "@implementation Foo\n+ (id)make:(int)a, ... { return nil; }\n@end", "method_definition", 0, + "(_:~)"}, + /* ARITY (dynamic tier-2 languages), shown on a Swift declaration. */ + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_ARITY, "class C { func upload(_ data: Data, to url: URL) {} }", + "function_declaration", 0, "(2)"}, +}; + +TEST(callable_sig_golden_table) { + for (size_t i = 0; i < sizeof(k_cases) / sizeof(k_cases[0]); i++) { + const sig_case_t *tc = &k_cases[i]; + CBMArena a; + cbm_arena_init(&a); + const char *sig = sig_of(&a, tc->lang, tc->mode, tc->source, tc->kind, tc->nth); + if (!sig || strcmp(sig, tc->expected) != 0) { + printf(" case %zu: got %s, want %s\n", i, sig ? sig : "(null)", tc->expected); + } + ASSERT_NOT_NULL(sig); + ASSERT_STR_EQ(sig, tc->expected); + ASSERT_TRUE(strlen(sig) <= CBM_CALLABLE_SIG_MAX); + /* Contract: no separator a leaf splitter uses, and the inverse + * recovers the base from a real-shaped QN. */ + ASSERT_NULL(strchr(sig, '.')); + ASSERT_NULL(strstr(sig, "::")); + ASSERT_NULL(strstr(sig, "->")); + char *qn = cbm_arena_sprintf(&a, "proj.pkg.Cls.name%s", sig); + ASSERT_EQ(cbm_qn_callable_base_len(qn), strlen("proj.pkg.Cls.name")); + cbm_arena_destroy(&a); + } + PASS(); +} + +/* Long signatures cap at CBM_CALLABLE_SIG_MAX with a hash of the FULL + * suffix: deterministic, still distinct, still invertible. */ +TEST(callable_sig_cap_keeps_identity) { + char src_a[4096]; + char src_b[4096]; + size_t n = (size_t)snprintf(src_a, sizeof(src_a), "class A { void f("); + for (int i = 0; i < 30; i++) { + n += (size_t)snprintf(src_a + n, sizeof(src_a) - n, "%sVeryLongParameterTypeName%d p%d", + i ? ", " : "", i, i); + } + memcpy(src_b, src_a, n); + snprintf(src_a + n, sizeof(src_a) - n, ", int last) {} }"); + snprintf(src_b + n, sizeof(src_b) - n, ", long last) {} }"); + CBMArena a; + cbm_arena_init(&a); + const char *sa = + sig_of(&a, CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, src_a, "method_declaration", 0); + const char *sa2 = + sig_of(&a, CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, src_a, "method_declaration", 0); + const char *sb = + sig_of(&a, CBM_LANG_JAVA, CBM_CALLABLE_ID_TYPED, src_b, "method_declaration", 0); + ASSERT_NOT_NULL(sa); + ASSERT_NOT_NULL(sb); + ASSERT_TRUE(strlen(sa) <= CBM_CALLABLE_SIG_MAX); + ASSERT_TRUE(strlen(sb) <= CBM_CALLABLE_SIG_MAX); + ASSERT_STR_EQ(sa, sa2); + ASSERT_STR_NEQ(sa, sb); /* they differ only past the cap: the hash tells */ + ASSERT_TRUE(strncmp(sa, "(VeryLongParameterTypeName0,", 28) == 0); + const char *hash = strchr(sa, '#'); + ASSERT_NOT_NULL(hash); + ASSERT_EQ(strlen(hash), (size_t)18); /* "#" + 16 hex + ")" */ + char *qn = cbm_arena_sprintf(&a, "p.A.f%s", sa); + ASSERT_EQ(cbm_qn_callable_base_len(qn), strlen("p.A.f")); + cbm_arena_destroy(&a); + PASS(); +} + +/* PR1 is plumbing: no language mints a suffix yet. */ +TEST(callable_sig_every_language_is_none) { + CBMArena a; + cbm_arena_init(&a); + const char *src = "class A { void f(int x) {} }"; + for (int lang = 0; lang < CBM_LANG_COUNT; lang++) { + ASSERT_EQ(cbm_callable_identity((CBMLanguage)lang), CBM_CALLABLE_ID_NONE); + } + TSParser *parser = ts_parser_new(); + ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_JAVA)); + TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)strlen(src)); + TSNode m = {0}; + int left = 0; + ASSERT_TRUE(find_nth(ts_tree_root_node(tree), "method_declaration", &left, &m)); + ASSERT_NULL(cbm_callable_sig(&a, m, src, CBM_LANG_JAVA)); + ts_tree_delete(tree); + ts_parser_delete(parser); + cbm_arena_destroy(&a); + PASS(); +} + +/* The inverse is the identity on every historical QN shape. */ +TEST(callable_sig_base_len_unsuffixed_is_full) { + static const char *const qns[] = { + "proj.pkg.Cls.method", + "proj.S.operator()", + "proj.S.operator<", + "proj.S.operator<=>", + "proj.S.operator->", + "proj.S.operator>>", + "proj.S.operator&&", + "proj.app/(auth)", + "proj.Makefile.$(OBJ)", + "proj.f.(anon)", + "", + "proj.x.list", + "proj.x.make_const", + "a::b::c", + "proj.~Dtor", + "(x)", + "proj.Makefile.foo$(EXT)", + "proj.lib/(group)/page", + "proj.C.+", + "proj.A.unary_!", + }; + for (size_t i = 0; i < sizeof(qns) / sizeof(qns[0]); i++) { + ASSERT_EQ(cbm_qn_callable_base_len(qns[i]), strlen(qns[i])); + } + ASSERT_EQ(cbm_qn_callable_base_len(NULL), (size_t)0); + PASS(); +} + +TEST(callable_sig_base_len_suffixed) { + static const struct { + const char *qn; + const char *base; + } rows[] = { + {"p.S.work(int,String)", "p.S.work"}, + {"p.S.work()", "p.S.work"}, + {"p.S.operator()()", "p.S.operator()"}, + {"p.S.operator()(int)const", "p.S.operator()"}, + {"p.S.operator<(T)", "p.S.operator<"}, + {"p.S.operator<=>(S)", "p.S.operator<=>"}, + {"p.S.operator->()", "p.S.operator->"}, + {"p.S.f(const vector&,int(*)(int))const&&", "p.S.f"}, + {"p.C.Work(ref int)", "p.C.Work"}, + {"p.Session.upload(_:Data,to:URL)", "p.Session.upload"}, + {"p.K.work(this:String,(Int)=>Unit)", "p.K.work"}, + {"p.O.initWithFrame(_:style:)", "p.O.initWithFrame"}, + {"p.C.+(l:C,r:C)", "p.C.+"}, + {"p.A.unary_!()", "p.A.unary_!"}, + {"p.S.operator/(S)", "p.S.operator/"}, + {"p.S.~S()", "p.S.~S"}, + {"p.J.f(VeryLong0,#0123456789abcdef)", "p.J.f"}, + }; + for (size_t i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + ASSERT_EQ(cbm_qn_callable_base_len(rows[i].qn), strlen(rows[i].base)); + } + PASS(); +} + +/* The leaf splitters look only at the base: Swift labels carry ':' and a + * suffix may carry '>' — neither is a separator. */ +TEST(callable_sig_leaf_splitters_skip_suffix) { + const char *swift = "p.Session.upload(_:Data,to:URL)"; + ASSERT_STR_EQ(cbm_lsp_bare_segment(swift), "upload(_:Data,to:URL)"); + ASSERT_STR_EQ(cbm_pipeline_qn_class_method_tail(swift), "Session.upload(_:Data,to:URL)"); + /* Unsuffixed spellings are unchanged. */ + ASSERT_STR_EQ(cbm_lsp_bare_segment("Math::square"), "square"); + ASSERT_STR_EQ(cbm_lsp_bare_segment("p->run"), "run"); + ASSERT_STR_EQ(cbm_lsp_bare_segment("identity"), "identity"); + ASSERT_STR_EQ(cbm_pipeline_qn_class_method_tail("p.q.Cls.m"), "Cls.m"); + PASS(); +} + +/* The registry's by-name bucket is the base leaf, so every overload of a + * signature-qualified callable is found by its bare name; an unsuffixed QN + * keeps its historical key (last '.' or "::" segment). */ +TEST(callable_sig_registry_by_name_uses_base_leaf) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "upload", "p.Session.upload(_:Data,to:URL)", "Method"); + cbm_registry_add(r, "upload", "p.Session.upload(_:Data,with:URLRequest)", "Method"); + cbm_registry_add(r, "square", "p.Math::square", "Function"); + const char **out = NULL; + int count = 0; + cbm_registry_find_by_name(r, "upload", &out, &count); + ASSERT_EQ(count, 2); + count = 0; + cbm_registry_find_by_name(r, "square", &out, &count); + ASSERT_EQ(count, 1); + cbm_registry_free(r); + PASS(); +} + +SUITE(callable_sig) { + RUN_TEST(callable_sig_golden_table); + RUN_TEST(callable_sig_cap_keeps_identity); + RUN_TEST(callable_sig_every_language_is_none); + RUN_TEST(callable_sig_base_len_unsuffixed_is_full); + RUN_TEST(callable_sig_base_len_suffixed); + RUN_TEST(callable_sig_leaf_splitters_skip_suffix); + RUN_TEST(callable_sig_registry_by_name_uses_base_leaf); +} diff --git a/tests/test_main.c b/tests/test_main.c index 31cceac608..0eae2997b3 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -785,6 +785,7 @@ extern void suite_subprocess(void); extern void suite_private_file_lock(void); extern void suite_lock_registry(void); extern void suite_extraction(void); +extern void suite_callable_sig(void); extern void suite_extraction_inheritance(void); extern void suite_extraction_imports(void); extern void suite_parse_coverage(void); @@ -1068,6 +1069,7 @@ int main(int argc, char **argv) { /* Existing C code regression tests */ RUN_SELECTED_SUITE(ac); RUN_SELECTED_SUITE(extraction); + RUN_SELECTED_SUITE(callable_sig); RUN_SELECTED_SUITE(extraction_inheritance); RUN_SELECTED_SUITE(extraction_imports); RUN_SELECTED_SUITE(parse_coverage); diff --git a/tests/test_store_nodes.c b/tests/test_store_nodes.c index cf7b8bc8fd..f19a1ef133 100644 --- a/tests/test_store_nodes.c +++ b/tests/test_store_nodes.c @@ -193,6 +193,101 @@ TEST(store_project_delete) { PASS(); } +/* ── QN suffix / callable-base lookups (#2061) ─────────────────── */ + +static void qn_lookup_node(cbm_store_t *s, const char *name, const char *qn) { + cbm_node_t n = {.project = "test", + .label = "Method", + .name = name, + .qualified_name = qn, + .file_path = "a.java", + .start_line = 1, + .end_line = 2}; + cbm_store_upsert_node(s, &n); +} + +/* The suffix tier matches its text literally: '_' is not a LIKE wildcard, + * so `my_func` must not also find `myXfunc`. */ +TEST(store_qn_suffix_is_literal) { + cbm_store_t *s = cbm_store_open_memory(); + cbm_store_upsert_project(s, "test", "/tmp/test"); + qn_lookup_node(s, "my_func", "test.a.my_func"); + qn_lookup_node(s, "myXfunc", "test.a.myXfunc"); + cbm_node_t *nodes = NULL; + int count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_qn_suffix(s, "test", "my_func", &nodes, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 1); + ASSERT_STR_EQ(nodes[0].qualified_name, "test.a.my_func"); + cbm_store_free_nodes(nodes, count); + cbm_store_close(s); + PASS(); +} + +/* A suffix longer than the old 512-byte pattern buffer is not truncated into + * a pattern that matches nothing. */ +TEST(store_qn_suffix_is_unbounded) { + cbm_store_t *s = cbm_store_open_memory(); + cbm_store_upsert_project(s, "test", "/tmp/test"); + char leaf[601]; + memset(leaf, 'a', sizeof(leaf) - 1); + leaf[sizeof(leaf) - 1] = '\0'; + char long_qn[640]; + char long_suffix[640]; + snprintf(long_qn, sizeof(long_qn), "test.m.%s", leaf); + snprintf(long_suffix, sizeof(long_suffix), "m.%s", leaf); + qn_lookup_node(s, leaf, long_qn); + + cbm_node_t *nodes = NULL; + int count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_qn_suffix(s, "test", long_suffix, &nodes, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 1); + ASSERT_STR_EQ(nodes[0].qualified_name, long_qn); + cbm_store_free_nodes(nodes, count); + cbm_store_close(s); + PASS(); +} + +/* A bare base QN (exact, or as a dotted suffix) finds every signature- + * qualified overload of it, and never an unsuffixed QN — so the tier is inert + * until a language mints suffixes. */ +TEST(store_qn_base_finds_overloads_only) { + cbm_store_t *s = cbm_store_open_memory(); + cbm_store_upsert_project(s, "test", "/tmp/test"); + qn_lookup_node(s, "work", "test.S.work(int)"); + qn_lookup_node(s, "work", "test.S.work(String)"); + qn_lookup_node(s, "work", "test.S.work"); + qn_lookup_node(s, "workx", "test.S.workx(int)"); + qn_lookup_node(s, "operator()", "test.S.operator()"); + qn_lookup_node(s, "Work", "test.C.Work(T)"); + + cbm_node_t *nodes = NULL; + int count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_qn_base(s, "test", "test.S.work", false, &nodes, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 2); + cbm_store_free_nodes(nodes, count); + + ASSERT_EQ(cbm_store_find_nodes_by_qn_base(s, "test", "S.work", true, &nodes, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 2); + cbm_store_free_nodes(nodes, count); + + ASSERT_EQ(cbm_store_find_nodes_by_qn_base(s, "test", "test.C.Work", false, &nodes, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 1); + cbm_store_free_nodes(nodes, count); + + /* `operator()` is a bare C++ leaf, not "operator" + "()". */ + ASSERT_EQ(cbm_store_find_nodes_by_qn_base(s, "test", "test.S.operator", false, &nodes, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 0); + cbm_store_free_nodes(nodes, count); + cbm_store_close(s); + PASS(); +} + /* ── Node CRUD ──────────────────────────────────────────────────── */ TEST(store_node_crud) { @@ -2362,6 +2457,9 @@ TEST(store_coverage_replace_rolls_back_when_shadow_rebuild_fails) { } SUITE(store_nodes) { + RUN_TEST(store_qn_suffix_is_literal); + RUN_TEST(store_qn_suffix_is_unbounded); + RUN_TEST(store_qn_base_finds_overloads_only); RUN_TEST(store_coverage_roundtrip_prune_shadow); RUN_TEST(store_coverage_targeted_path_and_scope_lookup); RUN_TEST(store_coverage_meta_zero_row_truncation_and_delete); From 7abaa91dea653af3e890759a63e40a09cfcb81d8 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 25 Sep 2026 22:38:35 +0200 Subject: [PATCH 02/49] chore(lint): clang-format the callable_sig.h include comment in store.c Signed-off-by: Martin Vogel --- src/store/store.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/store.c b/src/store/store.c index f4a2ab378d..c6b0c45725 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -78,7 +78,7 @@ enum { #include "foundation/compat.h" #include "foundation/log.h" #include "foundation/compat_regex.h" -#include "callable_sig.h" /* cbm_qn_callable_base_len: base-match tier */ +#include "callable_sig.h" /* cbm_qn_callable_base_len: base-match tier */ #include "foundation/mem_core.h" /* cbm_alloc: pattern buffers */ #include "foundation/str_util.h" From d8a845e0c5ef15e83bcd9ca15a0113cc495354f9 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 26 Sep 2026 13:26:02 +0800 Subject: [PATCH 03/49] fix(swift): preserve overload identity and label-aware call edges Enable typed, labeled Swift callable identities on the shared #2061 plumbing. Resolve calls by compatible labels, defaulted parameters, and trailing closures; retain all compatible overload edges with candidate counts. Cover registry, pipeline, and signature behavior. Refs #2061 Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 50 ++++++- internal/cbm/callable_sig.h | 5 + internal/cbm/cbm.h | 5 + internal/cbm/extract_calls.c | 33 +++++ internal/cbm/extract_defs.c | 27 ++++ internal/cbm/extract_unified.c | 11 ++ src/pipeline/pass_calls.c | 22 +++ src/pipeline/pass_definitions.c | 10 ++ src/pipeline/pass_parallel.c | 37 +++++ src/pipeline/pipeline.h | 10 ++ src/pipeline/pipeline_incremental.c | 17 +++ src/pipeline/registry.c | 201 ++++++++++++++++++++++++++++ src/store/store.h | 2 +- tests/test_callable_sig.c | 36 ++++- tests/test_pipeline.c | 175 ++++++++++++++++++++++++ tests/test_registry.c | 50 +++++++ 16 files changed, 680 insertions(+), 11 deletions(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 9d32f391a7..2233a950e5 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -32,11 +32,10 @@ static const uint64_t SIG_FNV_PRIME = 0x100000001b3ULL; static const char SIG_OPERATOR_CHARS[] = "+-*/%^&|~!=<>?[]"; CBMCallableIdentity cbm_callable_identity(CBMLanguage lang) { - /* Every language keeps its historical QN until its enable change lands - * (with the index-format bump that change carries). The planned modes: - * Java/Kotlin/C#/C++/CUDA/Scala TYPED, Swift LABELED_TYPED, ObjC LABELED, - * dynamic tier-2 languages ARITY. */ - (void)lang; + /* Each language enables identity with its own index-format bump. */ + if (lang == CBM_LANG_SWIFT) { + return CBM_CALLABLE_ID_LABELED_TYPED; + } return CBM_CALLABLE_ID_NONE; } @@ -737,6 +736,11 @@ static void sig_swift_param(sig_ctx_t *c, TSNode p) { if (ts_node_eq(ch, internal)) { continue; } + /* A default expression selects call arity, not type identity. */ + if ((field && strcmp(field, "default_value") == 0) || + sig_node_text_is(c, ch, "=")) { + break; + } const char *k = ts_node_type(ch); if (strcmp(k, "parameter_modifiers") == 0) { uint32_t mc = ts_node_named_child_count(ch); @@ -770,6 +774,42 @@ static void sig_swift_params(sig_ctx_t *c, TSNode node) { } } +uint64_t cbm_swift_default_mask(TSNode node, const char *source, uint8_t *count) { + (void)source; + uint64_t defaults = 0; + unsigned parameters = 0; + int last = -1; + uint32_t children = ts_node_child_count(node); + for (uint32_t i = 0; i < children; i++) { + TSNode ch = ts_node_child(node, i); + const char *kind = ts_node_type(ch); + if (strcmp(kind, "parameter") == 0) { + last = (int)parameters++; + if (last < 64) { + uint32_t pc = ts_node_child_count(ch); + for (uint32_t j = 0; j < pc; j++) { + TSNode part = ts_node_child(ch, j); + const char *pk = ts_node_type(part); + if (strcmp(pk, "=") == 0 || strstr(pk, "default_value") != NULL) { + defaults |= UINT64_C(1) << last; + break; + } + } + } + } else if (last >= 0 && last < 64 && strcmp(kind, "=") == 0) { + defaults |= UINT64_C(1) << last; + } else if (last >= 0 && (strcmp(kind, ")") == 0 || strcmp(kind, "function_body") == 0)) { + break; + } + } + if (count) { + /* ponytail: >64 parameters have no bitmask; use a dynamic mask if Swift + * code with that many parameters needs overload call resolution. */ + *count = parameters > 64 ? UINT8_MAX : (uint8_t)parameters; + } + return defaults; +} + /* ── Scala ─────────────────────────────────────────────────────── */ static void sig_scala_params(sig_ctx_t *c, TSNode node) { diff --git a/internal/cbm/callable_sig.h b/internal/cbm/callable_sig.h index c7b4d64fc9..da6b03319d 100644 --- a/internal/cbm/callable_sig.h +++ b/internal/cbm/callable_sig.h @@ -38,6 +38,7 @@ #include "arena.h" #include "tree_sitter/api.h" #include +#include typedef enum { CBM_CALLABLE_ID_NONE = 0, /* historical base QN, no suffix */ @@ -56,6 +57,10 @@ enum { CBM_CALLABLE_SIG_MAX = 200 }; * spell the new member under -Wmissing-field-initializers. */ CBMCallableIdentity cbm_callable_identity(CBMLanguage lang); +/* Swift parameter defaults are call-resolution metadata, not graph identity. + * Returns a bit per parameter; count > 64 is reported as 255. */ +uint64_t cbm_swift_default_mask(TSNode func_node, const char *source, uint8_t *count); + /* The identity suffix for the callable at `func_node` in the language's own * mode, or NULL when that mode is NONE (or the node carries no parameter * information the mode can use). Arena-owned. */ diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 40532848de..f3c4d8a825 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -242,6 +242,9 @@ typedef struct { * qualified_name (base QN = the first qn_sig_off bytes); 0 = no suffix. * Always 0 until a language enables its callable_identity mode. */ uint32_t qn_sig_off; + /* Swift only: defaulted parameters, indexed in declaration order. */ + uint64_t swift_default_mask; + uint8_t swift_param_count; } CBMDefinition; /* Argument captured from a call expression */ @@ -298,6 +301,8 @@ typedef struct { // (self.compiler.apply_converters()). An object the // class owns, not a parameter: read by the weak-member // guard's unique-name exemption. Default false. + bool swift_trailing_closure; + bool swift_args_truncated; } CBMCall; typedef struct { diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index e1de896b8c..921011f2ee 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -2061,6 +2061,8 @@ static void process_keyword_arg(CBMExtractCtx *ctx, TSNode arg_node, CBMCallArg } } +static TSNode swift_argument_value(TSNode arg); + /* Extract all arguments from a call expression into call->args[]. */ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) { uint32_t argc = ts_node_named_child_count(args); @@ -2085,6 +2087,26 @@ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) { CBMCallArg *ca = &call->args[call->arg_count]; memset(ca, 0, sizeof(*ca)); + if (ctx->language == CBM_LANG_SWIFT && strcmp(ak, "value_argument") == 0) { + TSNode first = ts_node_named_child(arg_node, 0); + if (!ts_node_is_null(first) && + strcmp(ts_node_type(first), "value_argument_label") == 0) { + TSNode label = ts_node_named_child(first, 0); + if (ts_node_is_null(label)) { + label = first; + } + ca->keyword = cbm_node_text(ctx->arena, label, ctx->source); + if (ca->keyword) { + size_t n = strlen(ca->keyword); + if (n > 0 && ca->keyword[n - 1] == ':') { + ca->keyword = cbm_arena_strndup(ctx->arena, ca->keyword, n - 1); + } + } + } + arg_node = swift_argument_value(arg_node); + ak = ts_node_type(arg_node); + } + if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) { process_keyword_arg(ctx, arg_node, ca); ca->index = positional_idx++; @@ -3831,6 +3853,17 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML if (ts_node_is_null(args) && ctx->language == CBM_LANG_SWIFT) { args = swift_call_args(node); } + if (ctx->language == CBM_LANG_SWIFT) { + TSNode suffix = cbm_find_child_by_kind(node, "call_suffix"); + if (!ts_node_is_null(suffix)) { + call.swift_trailing_closure = + !ts_node_is_null(cbm_find_child_by_kind(suffix, "lambda_literal")); + } + if (!ts_node_is_null(args) && + ts_node_named_child_count(args) > CBM_MAX_CALL_ARGS) { + call.swift_args_truncated = true; + } + } if (!ts_node_is_null(args)) { call.first_string_arg = extract_url_or_topic_arg(ctx, args); /* #952: routes registered inside Laravel `prefix()->group()` diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 916f1136f9..b0686bf4b6 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -1,4 +1,5 @@ #include "cbm.h" +#include "callable_sig.h" #include "arena.h" // CBMArena, cbm_arena_alloc/strdup/sprintf #include "helpers.h" #include "lang_specs.h" @@ -3944,6 +3945,20 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec def.is_test = rust_def_is_test(def.decorators); } + if (ctx->language == CBM_LANG_SWIFT) { + const char *sig = cbm_callable_sig(a, func_node, ctx->source, ctx->language); + if (!sig) { + return; + } + def.qn_sig_off = (uint32_t)strlen(def.qualified_name); + def.qualified_name = cbm_arena_sprintf(a, "%s%s", def.qualified_name, sig); + if (!def.qualified_name) { + return; + } + def.swift_default_mask = + cbm_swift_default_mask(func_node, ctx->source, &def.swift_param_count); + } + // C++/CUDA: GoogleTest macros are test functions (#1266). if (is_gtest) { def.is_test = true; @@ -5091,6 +5106,18 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node, memset(&def, 0, sizeof(def)); def.name = name; def.qualified_name = method_qn; + if (ctx->language == CBM_LANG_SWIFT) { + const char *sig = cbm_callable_sig(a, child, ctx->source, ctx->language); + if (!sig) { + return; + } + def.qn_sig_off = (uint32_t)strlen(method_qn); + def.qualified_name = cbm_arena_sprintf(a, "%s%s", method_qn, sig); + if (!def.qualified_name) { + return; + } + def.swift_default_mask = cbm_swift_default_mask(child, ctx->source, &def.swift_param_count); + } def.label = "Method"; def.file_path = ctx->rel_path; def.parent_class = class_qn; diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index 086f501a0c..402dbf3635 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -1,6 +1,7 @@ #include "extract_unified.h" #include "arena.h" // cbm_arena_sprintf #include "cbm.h" // CBMExtractCtx +#include "callable_sig.h" #include "helpers.h" #include "lang_specs.h" // CBMLangSpec, cbm_lang_spec, CBM_LANG_* #include "tree_sitter/api.h" // TSNode, TSTreeCursor, ts_tree_cursor_*, ts_node_* @@ -1092,6 +1093,16 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan * an in-body call sources to a QN one or more segments short of the def, and * the edge is dropped at write. */ const char *qn_name = name; + if (ctx->language == CBM_LANG_SWIFT) { + const char *sig = cbm_callable_sig(ctx->arena, node, ctx->source, ctx->language); + if (!sig) { + return NULL; + } + qn_name = cbm_arena_sprintf(ctx->arena, "%s%s", name, sig); + if (!qn_name) { + return NULL; + } + } if (ctx->language == CBM_LANG_NIX) { qn_name = cbm_nix_qn_name(ctx->arena, node, ctx->source, name); if (!qn_name || !qn_name[0]) { diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 84958954cf..3b771e7034 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -544,6 +544,28 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, } } + if (lang == CBM_LANG_SWIFT) { + const char *candidates[CBM_SZ_256]; + int count = cbm_registry_swift_candidates(ctx->registry, call, module_qn, imp_vals, + imp_count, candidates, CBM_SZ_256); + if (count != 0) { + int emitted = 0; + for (int i = 0; i < count; i++) { + const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, candidates[i]); + if (target && target->id != source_node->id) { + cbm_resolution_t selected = {.qualified_name = candidates[i], + .strategy = "swift_labels", + .confidence = count == 1 ? 0.90 : 0.55, + .candidate_count = count}; + emit_classified_edge(ctx, call, source_node, target, &selected, module_qn, + imp_keys, imp_vals, imp_count, false); + emitted++; + } + } + return emitted > 0 ? SKIP_ONE : 0; + } + } + cbm_resolution_t res = cbm_registry_resolve(ctx->registry, call->callee_name, module_qn, imp_keys, imp_vals, imp_count); if (!res.qualified_name || res.qualified_name[0] == '\0') { diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 7c7ed68847..f176d03d59 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -284,6 +284,12 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) return; } size_t pos = (size_t)n; + if (def->qn_sig_off && bufsize - pos > 80) { + pos += (size_t)snprintf(buf + pos, bufsize - pos, + ",\"swift_defaults\":\"%016llx\",\"swift_params\":%u", + (unsigned long long)def->swift_default_mask, + (unsigned)def->swift_param_count); + } append_json_string(buf, bufsize, &pos, "docstring", def->docstring); append_json_string(buf, bufsize, &pos, "signature", def->signature); append_json_string(buf, bufsize, &pos, "return_type", def->return_type); @@ -336,6 +342,10 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const * through the same predicate, so the three registries cannot diverge. */ if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) { cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label); + if (def->qn_sig_off) { + cbm_registry_set_swift_signature(ctx->registry, def->qualified_name, + def->swift_default_mask, def->swift_param_count); + } } char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__"); const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index e85ad015f1..33d8186d5f 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -512,6 +512,12 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) return; } size_t pos = (size_t)n; + if (def->qn_sig_off && bufsize - pos > 80) { + pos += (size_t)snprintf(buf + pos, bufsize - pos, + ",\"swift_defaults\":\"%016llx\",\"swift_params\":%u", + (unsigned long long)def->swift_default_mask, + (unsigned)def->swift_param_count); + } append_json_string(buf, bufsize, &pos, "docstring", def->docstring); append_json_string(buf, bufsize, &pos, "signature", def->signature); append_json_string(buf, bufsize, &pos, "return_type", def->return_type); @@ -1589,6 +1595,10 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d * (helpers.c) — see pass_definitions.c for the per-label rationale. */ if (cbm_label_is_registry_symbol(def->label)) { cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label); + if (def->qn_sig_off) { + cbm_registry_set_swift_signature(ctx->registry, def->qualified_name, + def->swift_default_mask, def->swift_param_count); + } (*reg_entries)++; } const cbm_gbuf_node_t *def_node = cbm_gbuf_find_by_qn(ctx->gbuf, def->qualified_name); @@ -2984,6 +2994,33 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB } } + if (lang == CBM_LANG_SWIFT && !lsp_target) { + const char *candidates[CBM_SZ_256]; + int count = cbm_registry_swift_candidates(rc->registry, call, module_qn, imp_vals, + imp_count, candidates, CBM_SZ_256); + if (count > 0) { + for (int i = 0; i < count; i++) { + const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(rc->main_gbuf, + candidates[i]); + if (!target || target->id == source_node->id) { + continue; + } + cbm_resolution_t selected = {.qualified_name = candidates[i], + .strategy = "swift_labels", + .confidence = count == 1 ? 0.90 : 0.55, + .candidate_count = count}; + emit_service_edge(ws->local_edge_buf, source_node, target, call, &selected, + module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals, + imp_count, false); + ws->calls_resolved++; + } + continue; + } + if (count < 0) { + res = (cbm_resolution_t){0}; + } + } + if (!res.qualified_name || res.qualified_name[0] == '\0') { if (cbm_service_pattern_route_method(call->callee_name) != NULL) { cbm_resolution_t fake_res = {.qualified_name = call->callee_name, diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index e5a672cb0b..cb0daf2a96 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -20,6 +20,7 @@ #include #include "discover/discover.h" /* cbm_ignored_file_t (#963) */ +#include "cbm.h" /* CBMCall for Swift overload selection */ #include "foundation/constants.h" /* CBM_SZ_512 */ /* Forward declarations */ @@ -225,6 +226,15 @@ void cbm_registry_free(cbm_registry_t *r); void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, const char *label); +/* Swift-only metadata and label-compatible overload selection. The returned + * QNs are borrowed from the registry and all belong to one base callable. + * -1 means this name has Swift definitions but no compatible overload. */ +void cbm_registry_set_swift_signature(cbm_registry_t *r, const char *qualified_name, + uint64_t default_mask, uint8_t param_count); +int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, + const char *module_qn, const char **import_vals, + int import_count, const char **out, int out_cap); + /* Resolve a callee name using prioritized strategies. * import_map: NULL-terminated array of {local_name, resolved_qn} pairs, or NULL. * Returns result with qualified_name="" if unresolved. diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 894b732572..957e9b869f 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1042,6 +1042,23 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { return; } cbm_registry_add(r, node->name, node->qualified_name, node->label); + if (node->properties_json) { + const char *mask = strstr(node->properties_json, "\"swift_defaults\":\""); + const char *count = strstr(node->properties_json, "\"swift_params\":"); + if (mask && count) { + mask += strlen("\"swift_defaults\":\""); + count += strlen("\"swift_params\":"); + char *mask_end = NULL; + char *count_end = NULL; + unsigned long long defaults = strtoull(mask, &mask_end, 16); + unsigned long params = strtoul(count, &count_end, 10); + if (mask_end == mask + 16 && *mask_end == '"' && count_end != count && + params <= UINT8_MAX) { + cbm_registry_set_swift_signature(r, node->qualified_name, (uint64_t)defaults, + (uint8_t)params); + } + } + } } static void free_incremental_result_cache(CBMFileResult **cache, int count) { diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 2d0a79a12b..1420562630 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -86,6 +86,11 @@ typedef struct { int is_test_cap; } qn_array_t; +typedef struct { + uint64_t defaults; + uint8_t count; +} swift_signature_t; + struct cbm_registry { /* Interned label strings (<=~30 distinct labels; owned here, freed in * _free). The exact map's VALUES point into this pool instead of one @@ -97,6 +102,7 @@ struct cbm_registry { /* byName: simpleName → qn_array_t* (heap-owned) */ CBMHashTable *by_name; + CBMHashTable *swift_signatures; /* borrowed exact-map keys; owned values */ }; /* ── Helpers ─────────────────────────────────────────────────────── */ @@ -842,6 +848,12 @@ static void free_qn_array(const char *key, void *value, void *ud) { free((void *)key); } +static void free_swift_signature(const char *key, void *value, void *ud) { + (void)key; + (void)ud; + free(value); +} + void cbm_registry_free(cbm_registry_t *r) { if (!r) { return; @@ -849,6 +861,10 @@ void cbm_registry_free(cbm_registry_t *r) { /* by_name first: its items borrow exact's keys. */ cbm_ht_foreach(r->by_name, free_qn_array, NULL); cbm_ht_free(r->by_name); + if (r->swift_signatures) { + cbm_ht_foreach(r->swift_signatures, free_swift_signature, NULL); + cbm_ht_free(r->swift_signatures); + } cbm_ht_foreach(r->exact, free_label, NULL); cbm_ht_free(r->exact); for (int i = 0; i < r->label_pool_n; i++) { @@ -924,6 +940,33 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified } } +void cbm_registry_set_swift_signature(cbm_registry_t *r, const char *qualified_name, + uint64_t default_mask, uint8_t param_count) { + if (!r || !qualified_name) { + return; + } + const char *owned = cbm_ht_get_key(r->exact, qualified_name); + if (!owned) { + return; + } + if (!r->swift_signatures) { + r->swift_signatures = cbm_ht_create(CBM_SZ_512); + if (!r->swift_signatures) { + return; + } + } + swift_signature_t *sig = cbm_ht_get(r->swift_signatures, owned); + if (!sig) { + sig = malloc(sizeof(*sig)); + if (!sig) { + return; + } + cbm_ht_set(r->swift_signatures, owned, sig); + } + sig->defaults = default_mask; + sig->count = param_count; +} + /* ── Lookup ──────────────────────────────────────────────────────── */ bool cbm_registry_exists(const cbm_registry_t *r, const char *qn) { @@ -1404,6 +1447,164 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle return res; } +/* Read labels from the signature-qualified QN. Types may contain commas in + * tuples, generic arguments or function types, so only top-level commas split + * parameters. The type text is used only to recognize a trailing closure. */ +static bool swift_signature_matches(const char *qn, const char *name, + const swift_signature_t *meta, const CBMCall *call) { + if (!meta || meta->count == UINT8_MAX || call->swift_args_truncated) { + return false; + } + size_t base_len = cbm_qn_callable_base_len_named(qn, name); + const char *sig = qn + base_len; + size_t n = strlen(sig); + if (n < 2 || sig[0] != '(' || sig[n - 1] != ')' || strchr(sig, '#')) { + return false; + } + const char *labels[64]; + size_t label_lens[64]; + bool closures[64]; + unsigned count = 0; + const char *entry = sig + 1; + const char *end = sig + n - 1; + int parens = 0, brackets = 0, angles = 0; + for (const char *p = entry; p <= end; p++) { + char ch = *p; + if (p == end || (ch == ',' && parens == 0 && brackets == 0 && angles == 0)) { + if (p > entry) { + if (count >= 64) { + return false; + } + const char *colon = memchr(entry, ':', (size_t)(p - entry)); + if (!colon) { + return false; + } + labels[count] = entry; + label_lens[count] = (size_t)(colon - entry); + closures[count] = false; + for (const char *t = colon + 1; t + 1 < p; t++) { + if (t[0] == '=' && t[1] == '>') { + closures[count] = true; + break; + } + } + count++; + } + entry = p + 1; + } else if (ch == '(') { + parens++; + } else if (ch == ')' && parens > 0) { + parens--; + } else if (ch == '[') { + brackets++; + } else if (ch == ']' && brackets > 0) { + brackets--; + } else if (ch == '<') { + angles++; + } else if (ch == '>' && angles > 0) { + angles--; + } + } + if (count != meta->count) { + return false; + } + int arg = 0; + bool used_closure = false; + for (unsigned i = 0; i < count; i++) { + const char *given = arg < call->arg_count ? call->args[arg].keyword : NULL; + if (!given) { + given = "_"; + } + if (arg < call->arg_count && strlen(given) == label_lens[i] && + memcmp(given, labels[i], label_lens[i]) == 0) { + arg++; + } else if (call->swift_trailing_closure && !used_closure && arg == call->arg_count && + i + 1 == count && closures[i]) { + used_closure = true; + } else if ((meta->defaults & (UINT64_C(1) << i)) == 0) { + return false; + } + } + return arg == call->arg_count && (!call->swift_trailing_closure || used_closure); +} + +int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, + const char *module_qn, const char **import_vals, + int import_count, const char **out, int out_cap) { + if (!r || !r->swift_signatures || !call || !call->callee_name || !out || out_cap < 1) { + return 0; + } + const char *name = simple_name(call->callee_name); + qn_array_t *bucket = cbm_ht_get(r->by_name, name); + if (!bucket || bucket->count > REG_MAX_CANDIDATES) { + return 0; + } + const char *matches[REG_MAX_CANDIDATES]; + int matched = 0; + bool has_swift = false; + for (int i = 0; i < bucket->count; i++) { + const char *qn = bucket->items[i]; + swift_signature_t *meta = cbm_ht_get(r->swift_signatures, qn); + has_swift |= meta != NULL; + if (meta && receiver_chain_admits(call->callee_name, qn) && + swift_signature_matches(qn, name, meta, call)) { + matches[matched++] = qn; + } + } + if (matched == 0) { + return has_swift ? -1 : 0; + } + /* Preserve the receiver when it names a class or namespace explicitly. */ + const char *chosen = NULL; + size_t callee_len = strlen(call->callee_name); + if (strchr(call->callee_name, '.')) { + for (int i = 0; i < matched; i++) { + size_t base = cbm_qn_callable_base_len_named(matches[i], name); + if (base >= callee_len && + memcmp(matches[i] + base - callee_len, call->callee_name, callee_len) == 0 && + (base == callee_len || matches[i][base - callee_len - 1] == '.')) { + chosen = matches[i]; + break; + } + } + } + if (!chosen) { + const char *reachable[REG_MAX_CANDIDATES]; + int reachable_count = 0; + if (import_vals) { + for (int i = 0; i < matched; i++) { + if (is_import_reachable(matches[i], import_vals, import_count)) { + reachable[reachable_count++] = matches[i]; + } + } + } + if (reachable_count > 0) { + chosen = module_qn ? best_by_import_distance(reachable, NULL, reachable_count, + module_qn) + : reachable[0]; + } + } + if (!chosen) { + chosen = module_qn ? best_by_import_distance(matches, NULL, matched, module_qn) + : matches[0]; + } + if (!chosen) { + return 0; + } + size_t chosen_base = cbm_qn_callable_base_len_named(chosen, name); + int found = 0; + for (int i = 0; i < matched; i++) { + size_t base = cbm_qn_callable_base_len_named(matches[i], name); + if (base == chosen_base && memcmp(matches[i], chosen, base) == 0) { + if (found == out_cap) { + return -1; + } + out[found++] = matches[i]; + } + } + return found; +} + cbm_resolution_t cbm_registry_resolve_lineage(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count) { diff --git a/src/store/store.h b/src/store/store.h index 3e387222de..e51f7d58df 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -23,7 +23,7 @@ typedef struct cbm_store cbm_store_t; #define CBM_STORE_OK 0 #define CBM_STORE_ERR (-1) #define CBM_STORE_NOT_FOUND (-2) -#define CBM_INDEX_FORMAT_VERSION 1 +#define CBM_INDEX_FORMAT_VERSION 2 #define CBM_STORE_CANCELLED (-3) #define CBM_STORE_SCAN_LIMIT (-4) #define CBM_STORE_CALLBACK_ERR (-5) diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index ad0315032a..07a5ed8df7 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -1,6 +1,6 @@ /* * test_callable_sig.c — golden tables for the signature-qualified callable - * identity builder and its inverse (#2061, plumbing: no language enabled). + * identity builder and its inverse (#2061). */ #include "test_framework.h" #include "callable_sig.h" @@ -223,13 +223,15 @@ TEST(callable_sig_cap_keeps_identity) { PASS(); } -/* PR1 is plumbing: no language mints a suffix yet. */ -TEST(callable_sig_every_language_is_none) { +/* Swift is enabled; the other languages keep their historical QNs. */ +TEST(callable_sig_only_swift_enabled) { CBMArena a; cbm_arena_init(&a); const char *src = "class A { void f(int x) {} }"; for (int lang = 0; lang < CBM_LANG_COUNT; lang++) { - ASSERT_EQ(cbm_callable_identity((CBMLanguage)lang), CBM_CALLABLE_ID_NONE); + ASSERT_EQ(cbm_callable_identity((CBMLanguage)lang), + lang == CBM_LANG_SWIFT ? CBM_CALLABLE_ID_LABELED_TYPED + : CBM_CALLABLE_ID_NONE); } TSParser *parser = ts_parser_new(); ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_JAVA)); @@ -244,6 +246,29 @@ TEST(callable_sig_every_language_is_none) { PASS(); } +TEST(callable_sig_swift_default_parameters) { + const char *src = "class C { func f(a: Int, b: String = \"x\", c: () -> Void) {} }"; + TSParser *parser = ts_parser_new(); + ASSERT_NOT_NULL(parser); + ASSERT_TRUE(ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_SWIFT))); + TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)strlen(src)); + ASSERT_NOT_NULL(tree); + TSNode method = {0}; + int left = 0; + ASSERT_TRUE(find_nth(ts_tree_root_node(tree), "function_declaration", &left, &method)); + CBMArena a; + cbm_arena_init(&a); + ASSERT_STR_EQ(cbm_callable_sig(&a, method, src, CBM_LANG_SWIFT), + "(a:Int,b:String,c:()=>Void)"); + uint8_t count = 0; + ASSERT_EQ(cbm_swift_default_mask(method, src, &count), UINT64_C(1) << 1); + ASSERT_EQ(count, 3); + cbm_arena_destroy(&a); + ts_tree_delete(tree); + ts_parser_delete(parser); + PASS(); +} + /* The inverse is the identity on every historical QN shape. */ TEST(callable_sig_base_len_unsuffixed_is_full) { static const char *const qns[] = { @@ -340,7 +365,8 @@ TEST(callable_sig_registry_by_name_uses_base_leaf) { SUITE(callable_sig) { RUN_TEST(callable_sig_golden_table); RUN_TEST(callable_sig_cap_keeps_identity); - RUN_TEST(callable_sig_every_language_is_none); + RUN_TEST(callable_sig_only_swift_enabled); + RUN_TEST(callable_sig_swift_default_parameters); RUN_TEST(callable_sig_base_len_unsuffixed_is_full); RUN_TEST(callable_sig_base_len_suffixed); RUN_TEST(callable_sig_leaf_splitters_skip_suffix); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index d32ab3538b..f41da44a0a 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -6844,6 +6844,180 @@ TEST(pipeline_swift_http_call_makes_route_issue1892) { PASS(); } +static int pipeline_has_calls_edge(cbm_store_t *s, int64_t source_id, int64_t target_id) { + cbm_edge_t *edges = NULL; + int edge_count = 0; + if (cbm_store_find_edges_by_source_type(s, source_id, "CALLS", &edges, &edge_count) != + CBM_STORE_OK) { + return -1; + } + + int found = 0; + for (int i = 0; i < edge_count; i++) { + if (edges[i].target_id == target_id) { + found = 1; + break; + } + } + cbm_store_free_edges(edges, edge_count); + return found; +} + +/* #2061: distinct signature QNs keep each overload's CALLS edges separate. */ +TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftoverload_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + + write_temp_file(tmp, "Sources/Sink.swift", + "class Sink {\n" + " func target() {}\n" + "}\n"); + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " let sink = Sink()\n" + "\n" + " // Overload A: DOES call target()\n" + " func work(flag: Bool) {\n" + " self.sink.target()\n" + " }\n" + "\n" + " // Overload B: does NOT call target()\n" + " func work(name: String) {\n" + " print(name)\n" + " }\n" + " func upload(_ data: Data, to url: URL) { sink.target() }\n" + " func upload(_ fileURL: URL, to url: URL) { print(fileURL) }\n" + " func withDefault(message: String, flag: Bool = true) { sink.target() }\n" + " func withCallback(completion: () -> Void) { sink.target() }\n" + "}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + "\n" + " // Calls ONLY overload B, which never reaches target()\n" + " func onlyCallsOverloadB() {\n" + " self.service.work(name: \"x\")\n" + " }\n" + " func callAmbiguousUpload() {\n" + " self.service.upload(Data(), to: URL(string: \"/x\")!)\n" + " }\n" + " func callWithDefault() { self.service.withDefault(message: \"x\") }\n" + " func callWithClosure() { self.service.withCallback { } }\n" + "}\n"); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swiftoverload.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + cbm_node_t *works = NULL; + int work_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "work", &works, &work_count), CBM_STORE_OK); + ASSERT_EQ(work_count, 2); + cbm_store_free_nodes(works, work_count); + cbm_node_t *uploads = NULL; + int upload_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "upload", &uploads, &upload_count), + CBM_STORE_OK); + ASSERT_EQ(upload_count, 2); + cbm_store_free_nodes(uploads, upload_count); + + char flag_qn[512]; + char name_qn[512]; + char target_qn[512]; + char caller_qn[512]; + snprintf(flag_qn, sizeof(flag_qn), "%s.Sources.Service.Service.work(flag:Bool)", project); + snprintf(name_qn, sizeof(name_qn), "%s.Sources.Service.Service.work(name:String)", project); + snprintf(target_qn, sizeof(target_qn), "%s.Sources.Sink.Sink.target()", project); + snprintf(caller_qn, sizeof(caller_qn), "%s.Sources.Caller.Caller.onlyCallsOverloadB()", project); + + cbm_node_t flag = {0}; + cbm_node_t name = {0}; + cbm_node_t target = {0}; + cbm_node_t caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, flag_qn, &flag), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, name_qn, &name), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, target_qn, &target), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, caller_qn, &caller), CBM_STORE_OK); + ASSERT_EQ(flag.start_line, 5); + ASSERT_EQ(flag.end_line, 7); + ASSERT_EQ(name.start_line, 10); + ASSERT_EQ(name.end_line, 12); + + ASSERT_EQ(pipeline_has_calls_edge(s, flag.id, target.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, name.id, target.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, name.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, flag.id), 0); + + char data_qn[512], url_qn[512], upload_caller_qn[512]; + char default_qn[512], default_caller_qn[512], closure_qn[512], closure_caller_qn[512]; + snprintf(data_qn, sizeof(data_qn), "%s.Sources.Service.Service.upload(_:Data,to:URL)", + project); + snprintf(url_qn, sizeof(url_qn), "%s.Sources.Service.Service.upload(_:URL,to:URL)", project); + snprintf(upload_caller_qn, sizeof(upload_caller_qn), + "%s.Sources.Caller.Caller.callAmbiguousUpload()", project); + snprintf(default_qn, sizeof(default_qn), + "%s.Sources.Service.Service.withDefault(message:String,flag:Bool)", project); + snprintf(default_caller_qn, sizeof(default_caller_qn), + "%s.Sources.Caller.Caller.callWithDefault()", project); + snprintf(closure_qn, sizeof(closure_qn), + "%s.Sources.Service.Service.withCallback(completion:()=>Void)", project); + snprintf(closure_caller_qn, sizeof(closure_caller_qn), + "%s.Sources.Caller.Caller.callWithClosure()", project); + cbm_node_t data = {0}, url = {0}, upload_caller = {0}; + cbm_node_t with_default = {0}, default_caller = {0}, with_closure = {0}, closure_caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, data_qn, &data), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, url_qn, &url), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, upload_caller_qn, &upload_caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, default_qn, &with_default), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, default_caller_qn, &default_caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, closure_qn, &with_closure), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, closure_caller_qn, &closure_caller), CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(s, upload_caller.id, data.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, upload_caller.id, url.id), 1); + cbm_edge_t *upload_edges = NULL; + int upload_edges_count = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, upload_caller.id, "CALLS", &upload_edges, + &upload_edges_count), + CBM_STORE_OK); + int ambiguous_edges = 0; + for (int i = 0; i < upload_edges_count; i++) { + if ((upload_edges[i].target_id == data.id || upload_edges[i].target_id == url.id) && + upload_edges[i].properties_json && + strstr(upload_edges[i].properties_json, "\"candidates\":2")) { + ambiguous_edges++; + } + } + ASSERT_EQ(ambiguous_edges, 2); + cbm_store_free_edges(upload_edges, upload_edges_count); + ASSERT_EQ(pipeline_has_calls_edge(s, default_caller.id, with_default.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, closure_caller.id, with_closure.id), 1); + cbm_node_free_fields(&data); + cbm_node_free_fields(&url); + cbm_node_free_fields(&upload_caller); + cbm_node_free_fields(&with_default); + cbm_node_free_fields(&default_caller); + cbm_node_free_fields(&with_closure); + cbm_node_free_fields(&closure_caller); + + cbm_node_free_fields(&flag); + cbm_node_free_fields(&name); + cbm_node_free_fields(&target); + cbm_node_free_fields(&caller); + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + /* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's * resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls * but forces the parallel resolver, since the empty-resolution fallback is a @@ -15132,6 +15306,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_native_fetch_classified_as_http_calls); RUN_TEST(pipeline_swift_nested_url_makes_route_issue1892); RUN_TEST(pipeline_swift_http_call_makes_route_issue1892); + RUN_TEST(pipeline_swift_overloads_keep_argument_labels_issue2061); RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls); RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http); /* Git history pass */ diff --git a/tests/test_registry.c b/tests/test_registry.c index 5f751a8993..00afec94eb 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1171,6 +1171,55 @@ TEST(resolve_import_map_alias_with_suffix_hits_method) { PASS(); } +TEST(swift_overload_labels_defaults_and_trailing_closure) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + const char *qns[] = { + "proj.Service.work(flag:Bool)", + "proj.Service.work(name:String)", + "proj.Service.work(name:Int)", + "proj.Other.work(name:String)", + "proj.Service.send(value:Int,completion:()=>Void)", + "proj.Service.configure(a:Int,b:Int,c:Int)", + }; + const char *names[] = {"work", "work", "work", "work", "send", "configure"}; + const uint8_t counts[] = {1, 1, 1, 1, 2, 3}; + for (int i = 0; i < 6; i++) { + cbm_registry_add(r, names[i], qns[i], "Method"); + cbm_registry_set_swift_signature(r, qns[i], i == 5 ? UINT64_C(1) << 1 : 0, counts[i]); + } + + const char *out[8] = {0}; + CBMCallArg args[2] = {{.keyword = "name"}}; + CBMCall call = {.callee_name = "Service.work", .args = args, .arg_count = 1}; + int n = cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8); + ASSERT_EQ(n, 2); /* type is unknown: keep both compatible overloads */ + ASSERT_TRUE((strcmp(out[0], qns[1]) == 0 && strcmp(out[1], qns[2]) == 0) || + (strcmp(out[0], qns[2]) == 0 && strcmp(out[1], qns[1]) == 0)); + + args[0].keyword = "flag"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[0]); + args[0].keyword = "missing"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + + args[0].keyword = "a"; + args[1].keyword = "c"; + call.callee_name = "configure"; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[5]); + + args[0].keyword = "value"; + call.callee_name = "send"; + call.arg_count = 1; + call.swift_trailing_closure = true; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[4]); + cbm_registry_free(r); + PASS(); +} + SUITE(registry) { /* FQN */ RUN_TEST(fqn_simple); @@ -1206,6 +1255,7 @@ SUITE(registry) { RUN_TEST(resolve_import_map_bare_alias); RUN_TEST(resolve_import_map_aliased_from_import); RUN_TEST(resolve_import_map_alias_with_suffix_hits_method); + RUN_TEST(swift_overload_labels_defaults_and_trailing_closure); RUN_TEST(resolve_unique_name); RUN_TEST(resolve_unresolved); RUN_TEST(resolve_many_nodes); From 7ae83006798c73813fbfc80c318b9680bd134c02 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 26 Sep 2026 14:15:34 +0800 Subject: [PATCH 04/49] style(swift): format overload patch Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 3 +-- internal/cbm/extract_calls.c | 3 +-- src/pipeline/pass_definitions.c | 7 +++---- src/pipeline/pass_parallel.c | 11 +++++------ src/pipeline/pipeline.h | 4 ++-- src/pipeline/registry.c | 18 +++++++++--------- tests/test_callable_sig.c | 6 ++---- tests/test_pipeline.c | 15 +++++++++------ 8 files changed, 32 insertions(+), 35 deletions(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 2233a950e5..6e2c8b08e4 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -737,8 +737,7 @@ static void sig_swift_param(sig_ctx_t *c, TSNode p) { continue; } /* A default expression selects call arity, not type identity. */ - if ((field && strcmp(field, "default_value") == 0) || - sig_node_text_is(c, ch, "=")) { + if ((field && strcmp(field, "default_value") == 0) || sig_node_text_is(c, ch, "=")) { break; } const char *k = ts_node_type(ch); diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 921011f2ee..7c8d783d64 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -3859,8 +3859,7 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML call.swift_trailing_closure = !ts_node_is_null(cbm_find_child_by_kind(suffix, "lambda_literal")); } - if (!ts_node_is_null(args) && - ts_node_named_child_count(args) > CBM_MAX_CALL_ARGS) { + if (!ts_node_is_null(args) && ts_node_named_child_count(args) > CBM_MAX_CALL_ARGS) { call.swift_args_truncated = true; } } diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index f176d03d59..03dff2b973 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -285,10 +285,9 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) } size_t pos = (size_t)n; if (def->qn_sig_off && bufsize - pos > 80) { - pos += (size_t)snprintf(buf + pos, bufsize - pos, - ",\"swift_defaults\":\"%016llx\",\"swift_params\":%u", - (unsigned long long)def->swift_default_mask, - (unsigned)def->swift_param_count); + pos += (size_t)snprintf( + buf + pos, bufsize - pos, ",\"swift_defaults\":\"%016llx\",\"swift_params\":%u", + (unsigned long long)def->swift_default_mask, (unsigned)def->swift_param_count); } append_json_string(buf, bufsize, &pos, "docstring", def->docstring); append_json_string(buf, bufsize, &pos, "signature", def->signature); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 33d8186d5f..57c97b0e26 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -513,10 +513,9 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) } size_t pos = (size_t)n; if (def->qn_sig_off && bufsize - pos > 80) { - pos += (size_t)snprintf(buf + pos, bufsize - pos, - ",\"swift_defaults\":\"%016llx\",\"swift_params\":%u", - (unsigned long long)def->swift_default_mask, - (unsigned)def->swift_param_count); + pos += (size_t)snprintf( + buf + pos, bufsize - pos, ",\"swift_defaults\":\"%016llx\",\"swift_params\":%u", + (unsigned long long)def->swift_default_mask, (unsigned)def->swift_param_count); } append_json_string(buf, bufsize, &pos, "docstring", def->docstring); append_json_string(buf, bufsize, &pos, "signature", def->signature); @@ -3000,8 +2999,8 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB imp_count, candidates, CBM_SZ_256); if (count > 0) { for (int i = 0; i < count; i++) { - const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(rc->main_gbuf, - candidates[i]); + const cbm_gbuf_node_t *target = + cbm_gbuf_find_by_qn(rc->main_gbuf, candidates[i]); if (!target || target->id == source_node->id) { continue; } diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index cb0daf2a96..f6879a23b5 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -232,8 +232,8 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified void cbm_registry_set_swift_signature(cbm_registry_t *r, const char *qualified_name, uint64_t default_mask, uint8_t param_count); int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, - const char *module_qn, const char **import_vals, - int import_count, const char **out, int out_cap); + const char *module_qn, const char **import_vals, int import_count, + const char **out, int out_cap); /* Resolve a callee name using prioritized strategies. * import_map: NULL-terminated array of {local_name, resolved_qn} pairs, or NULL. diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 1420562630..cd8f0c2180 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1450,8 +1450,8 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle /* Read labels from the signature-qualified QN. Types may contain commas in * tuples, generic arguments or function types, so only top-level commas split * parameters. The type text is used only to recognize a trailing closure. */ -static bool swift_signature_matches(const char *qn, const char *name, - const swift_signature_t *meta, const CBMCall *call) { +static bool swift_signature_matches(const char *qn, const char *name, const swift_signature_t *meta, + const CBMCall *call) { if (!meta || meta->count == UINT8_MAX || call->swift_args_truncated) { return false; } @@ -1529,8 +1529,8 @@ static bool swift_signature_matches(const char *qn, const char *name, } int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, - const char *module_qn, const char **import_vals, - int import_count, const char **out, int out_cap) { + const char *module_qn, const char **import_vals, int import_count, + const char **out, int out_cap) { if (!r || !r->swift_signatures || !call || !call->callee_name || !out || out_cap < 1) { return 0; } @@ -1579,14 +1579,14 @@ int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, } } if (reachable_count > 0) { - chosen = module_qn ? best_by_import_distance(reachable, NULL, reachable_count, - module_qn) - : reachable[0]; + chosen = module_qn + ? best_by_import_distance(reachable, NULL, reachable_count, module_qn) + : reachable[0]; } } if (!chosen) { - chosen = module_qn ? best_by_import_distance(matches, NULL, matched, module_qn) - : matches[0]; + chosen = + module_qn ? best_by_import_distance(matches, NULL, matched, module_qn) : matches[0]; } if (!chosen) { return 0; diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index 07a5ed8df7..9e0d1952d5 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -230,8 +230,7 @@ TEST(callable_sig_only_swift_enabled) { const char *src = "class A { void f(int x) {} }"; for (int lang = 0; lang < CBM_LANG_COUNT; lang++) { ASSERT_EQ(cbm_callable_identity((CBMLanguage)lang), - lang == CBM_LANG_SWIFT ? CBM_CALLABLE_ID_LABELED_TYPED - : CBM_CALLABLE_ID_NONE); + lang == CBM_LANG_SWIFT ? CBM_CALLABLE_ID_LABELED_TYPED : CBM_CALLABLE_ID_NONE); } TSParser *parser = ts_parser_new(); ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_JAVA)); @@ -258,8 +257,7 @@ TEST(callable_sig_swift_default_parameters) { ASSERT_TRUE(find_nth(ts_tree_root_node(tree), "function_declaration", &left, &method)); CBMArena a; cbm_arena_init(&a); - ASSERT_STR_EQ(cbm_callable_sig(&a, method, src, CBM_LANG_SWIFT), - "(a:Int,b:String,c:()=>Void)"); + ASSERT_STR_EQ(cbm_callable_sig(&a, method, src, CBM_LANG_SWIFT), "(a:Int,b:String,c:()=>Void)"); uint8_t count = 0; ASSERT_EQ(cbm_swift_default_mask(method, src, &count), UINT64_C(1) << 1); ASSERT_EQ(count, 3); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index f41da44a0a..791e2db0e7 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -6937,7 +6937,8 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { snprintf(flag_qn, sizeof(flag_qn), "%s.Sources.Service.Service.work(flag:Bool)", project); snprintf(name_qn, sizeof(name_qn), "%s.Sources.Service.Service.work(name:String)", project); snprintf(target_qn, sizeof(target_qn), "%s.Sources.Sink.Sink.target()", project); - snprintf(caller_qn, sizeof(caller_qn), "%s.Sources.Caller.Caller.onlyCallsOverloadB()", project); + snprintf(caller_qn, sizeof(caller_qn), "%s.Sources.Caller.Caller.onlyCallsOverloadB()", + project); cbm_node_t flag = {0}; cbm_node_t name = {0}; @@ -6959,8 +6960,7 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { char data_qn[512], url_qn[512], upload_caller_qn[512]; char default_qn[512], default_caller_qn[512], closure_qn[512], closure_caller_qn[512]; - snprintf(data_qn, sizeof(data_qn), "%s.Sources.Service.Service.upload(_:Data,to:URL)", - project); + snprintf(data_qn, sizeof(data_qn), "%s.Sources.Service.Service.upload(_:Data,to:URL)", project); snprintf(url_qn, sizeof(url_qn), "%s.Sources.Service.Service.upload(_:URL,to:URL)", project); snprintf(upload_caller_qn, sizeof(upload_caller_qn), "%s.Sources.Caller.Caller.callAmbiguousUpload()", project); @@ -6976,11 +6976,14 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { cbm_node_t with_default = {0}, default_caller = {0}, with_closure = {0}, closure_caller = {0}; ASSERT_EQ(cbm_store_find_node_by_qn(s, project, data_qn, &data), CBM_STORE_OK); ASSERT_EQ(cbm_store_find_node_by_qn(s, project, url_qn, &url), CBM_STORE_OK); - ASSERT_EQ(cbm_store_find_node_by_qn(s, project, upload_caller_qn, &upload_caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, upload_caller_qn, &upload_caller), + CBM_STORE_OK); ASSERT_EQ(cbm_store_find_node_by_qn(s, project, default_qn, &with_default), CBM_STORE_OK); - ASSERT_EQ(cbm_store_find_node_by_qn(s, project, default_caller_qn, &default_caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, default_caller_qn, &default_caller), + CBM_STORE_OK); ASSERT_EQ(cbm_store_find_node_by_qn(s, project, closure_qn, &with_closure), CBM_STORE_OK); - ASSERT_EQ(cbm_store_find_node_by_qn(s, project, closure_caller_qn, &closure_caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, closure_caller_qn, &closure_caller), + CBM_STORE_OK); ASSERT_EQ(pipeline_has_calls_edge(s, upload_caller.id, data.id), 1); ASSERT_EQ(pipeline_has_calls_edge(s, upload_caller.id, url.id), 1); cbm_edge_t *upload_edges = NULL; From 20b55ff37aa52b9c750a39480bda6b0ee3ae406c Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 26 Sep 2026 14:17:11 +0800 Subject: [PATCH 05/49] test(swift): cover parallel overload candidate edges Force the parallel call pass on a Swift fixture and assert both label-compatible type overloads retain CALLS edges with the ambiguity count. Refs #2061 Signed-off-by: DavidHLP --- tests/test_pipeline.c | 74 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 791e2db0e7..7128e759fd 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -7021,6 +7021,79 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { PASS(); } +/* The parallel call pass must retain both type-only overload candidates. */ +TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftoverload_par_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " func upload(_ data: Data, to url: URL) {}\n" + " func upload(_ file: URL, to url: URL) {}\n" + "}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + " func invoke() { self.service.upload(Data(), to: URL(string: \"/x\")!) }\n" + "}\n"); + for (int i = 0; i < 50; i++) { + char path[64], source[80]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(source, sizeof(source), "func filler%d() {}\n", i); + write_temp_file(tmp, path, source); + } + + char *previous_workers = getenv("CBM_WORKERS"); + char *saved_workers = previous_workers ? strdup(previous_workers) : NULL; + cbm_setenv("CBM_WORKERS", "4", 1); + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swiftoverload.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + int run_result = p ? cbm_pipeline_run(p) : -1; + if (saved_workers) { + cbm_setenv("CBM_WORKERS", saved_workers, 1); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + free(saved_workers); + ASSERT_NOT_NULL(p); + ASSERT_EQ(run_result, 0); + const char *project = cbm_pipeline_project_name(p); + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + char caller_qn[512], data_qn[512], url_qn[512]; + snprintf(caller_qn, sizeof(caller_qn), "%s.Sources.Caller.Caller.invoke()", project); + snprintf(data_qn, sizeof(data_qn), "%s.Sources.Service.Service.upload(_:Data,to:URL)", project); + snprintf(url_qn, sizeof(url_qn), "%s.Sources.Service.Service.upload(_:URL,to:URL)", project); + cbm_node_t caller = {0}, data = {0}, url = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, caller_qn, &caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, data_qn, &data), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, url_qn, &url), CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, data.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, url.id), 1); + cbm_edge_t *edges = NULL; + int edge_count = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &edge_count), + CBM_STORE_OK); + int ambiguous = 0; + for (int i = 0; i < edge_count; i++) { + if ((edges[i].target_id == data.id || edges[i].target_id == url.id) && + edges[i].properties_json && strstr(edges[i].properties_json, "\"candidates\":2")) { + ambiguous++; + } + } + ASSERT_EQ(ambiguous, 2); + cbm_store_free_edges(edges, edge_count); + cbm_node_free_fields(&caller); + cbm_node_free_fields(&data); + cbm_node_free_fields(&url); + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + /* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's * resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls * but forces the parallel resolver, since the empty-resolution fallback is a @@ -15310,6 +15383,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_swift_nested_url_makes_route_issue1892); RUN_TEST(pipeline_swift_http_call_makes_route_issue1892); RUN_TEST(pipeline_swift_overloads_keep_argument_labels_issue2061); + RUN_TEST(pipeline_swift_overloads_parallel_candidates_issue2061); RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls); RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http); /* Git history pass */ From d5cb9b5e949e92178631cd1e2ce167f7830ee8e3 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 26 Sep 2026 15:52:52 +0800 Subject: [PATCH 06/49] test(swift): accept signature-qualified caller names in call matrix Signed-off-by: DavidHLP --- tests/repro/repro_call_argument_matrix_a.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/repro/repro_call_argument_matrix_a.c b/tests/repro/repro_call_argument_matrix_a.c index 5399bddab4..c3ef6023b8 100644 --- a/tests/repro/repro_call_argument_matrix_a.c +++ b/tests/repro/repro_call_argument_matrix_a.c @@ -17,6 +17,7 @@ */ #include "test_framework.h" #include "lang_specs.h" +#include "callable_sig.h" #include #include @@ -74,9 +75,9 @@ typedef struct { static int qn_has_terminal_name(const char *qn, const char *name) { if (!qn || !name) return 0; - size_t qn_len = strlen(qn); + size_t qn_len = cbm_qn_callable_base_len(qn); size_t name_len = strlen(name); - if (name_len > qn_len || strcmp(qn + qn_len - name_len, name) != 0) + if (name_len > qn_len || strncmp(qn + qn_len - name_len, name, name_len) != 0) return 0; if (name_len == qn_len) return 1; From 19c8d07e0503ed8f1df0c8165c8079f2316d9510 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Tue, 29 Sep 2026 21:42:15 +0800 Subject: [PATCH 07/49] fix(swift): unify incompatible-overload fallback across resolver paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Swift call that shares its bare name with project symbols but matches none of their overloads was handled differently by the two resolvers: the parallel path cleared the bare-name match and fell through as unresolved (keeping each resolver's empty-resolution service fallbacks), while the sequential path dropped the call outright. Skip the bare-name registry bind on the sequential path too — it would bind a wrong overload — and continue as unresolved, so count < 0 behaves exactly like any other unresolved call on each resolver. Pipeline regressions pin the global-fetch fallback (#856) on both paths: the incompatible local overload gets no wrong CALLS edge, and the URL call keeps its HTTP_CALLS edge. The route-registration fallback differs between the paths on main (sequential additionally requires a '/'-leading first argument); that pre-existing divergence is out of scope here. Refs #2061 Signed-off-by: DavidHLP --- src/pipeline/pass_calls.c | 28 +++++--- src/pipeline/pass_parallel.c | 4 ++ tests/test_pipeline.c | 128 +++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 8 deletions(-) diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 3b771e7034..70349473ff 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -544,19 +544,28 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, } } + /* Swift label-compatible overload selection (#2061). count > 0: emit an + * edge to every compatible candidate and stop. count < 0: project symbols + * share this name but none matches the call's labels — a bare-name + * registry match would bind a WRONG overload, so resolution is skipped + * and the call falls through as unresolved; the empty-resolution service + * fallbacks below still run, mirroring the Swift block in pass_parallel.c + * (service edges survive overload suppression, per #523/#606/#856). + * count == 0: no overload metadata for this name — resolve normally. */ + int swift_candidates = 0; if (lang == CBM_LANG_SWIFT) { const char *candidates[CBM_SZ_256]; - int count = cbm_registry_swift_candidates(ctx->registry, call, module_qn, imp_vals, - imp_count, candidates, CBM_SZ_256); - if (count != 0) { + swift_candidates = cbm_registry_swift_candidates(ctx->registry, call, module_qn, imp_vals, + imp_count, candidates, CBM_SZ_256); + if (swift_candidates > 0) { int emitted = 0; - for (int i = 0; i < count; i++) { + for (int i = 0; i < swift_candidates; i++) { const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, candidates[i]); if (target && target->id != source_node->id) { cbm_resolution_t selected = {.qualified_name = candidates[i], .strategy = "swift_labels", - .confidence = count == 1 ? 0.90 : 0.55, - .candidate_count = count}; + .confidence = swift_candidates == 1 ? 0.90 : 0.55, + .candidate_count = swift_candidates}; emit_classified_edge(ctx, call, source_node, target, &selected, module_qn, imp_keys, imp_vals, imp_count, false); emitted++; @@ -566,8 +575,11 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, } } - cbm_resolution_t res = cbm_registry_resolve(ctx->registry, call->callee_name, module_qn, - imp_keys, imp_vals, imp_count); + cbm_resolution_t res = {0}; + if (swift_candidates >= 0) { + res = cbm_registry_resolve(ctx->registry, call->callee_name, module_qn, imp_keys, imp_vals, + imp_count); + } if (!res.qualified_name || res.qualified_name[0] == '\0') { /* Resolution is empty when the callee belongs to an EXTERNAL client * library whose source is not in the indexed tree (e.g. `requests.get`, diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 57c97b0e26..4b30a1d1c5 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -3016,6 +3016,10 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB continue; } if (count < 0) { + /* Same-named Swift symbols exist but none is label-compatible: + * clear the bare-name match (it would bind a wrong overload) + * and fall through as unresolved — the empty-resolution + * fallbacks below still run. Mirrors pass_calls.c. */ res = (cbm_resolution_t){0}; } } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 7128e759fd..3d8b253321 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -7094,6 +7094,132 @@ TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { PASS(); } +/* #2061: a Swift call matching NONE of a same-named project symbol's + * overloads must not bind to it (a bare-name match would bind a wrong + * overload), while the empty-resolution service fallbacks still apply — + * the contract both resolver paths share (Swift blocks in pass_calls.c + * and pass_parallel.c). Local `fetch(id:)` cannot take a one-argument + * unlabeled URL call, so the call stays unresolved and classifies as the + * global API (#856). */ +TEST(pipeline_swift_incompatible_overload_unresolved_issue2061) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftincompat_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + + write_temp_file(tmp, "Sources/Api.swift", + "func fetch(id: Int) -> Int {\n" + " return id\n" + "}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " func run() -> Int {\n" + " return fetch(\"https://api.example.com/data\")\n" + " }\n" + "}\n"); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swiftincompat.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + /* The incompatible local definition IS indexed (anti-vacuous guard) ... */ + cbm_node_t *fetches = NULL; + int fetch_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "fetch", &fetches, &fetch_count), + CBM_STORE_OK); + ASSERT_EQ(fetch_count, 1); + cbm_node_t *runs = NULL; + int run_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "run", &runs, &run_count), CBM_STORE_OK); + ASSERT_EQ(run_count, 1); + /* ... but the call never binds to it ... */ + ASSERT_EQ(pipeline_has_calls_edge(s, runs[0].id, fetches[0].id), 0); + cbm_store_free_nodes(fetches, fetch_count); + cbm_store_free_nodes(runs, run_count); + + /* ... and the URL call still classifies as the global API. */ + ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +/* The parallel resolver must reach the same verdict for the same fixture + * (>= 50 files routes calls through resolve_file_calls). */ +TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftincompat_par_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + + write_temp_file(tmp, "Sources/Api.swift", + "func fetch(id: Int) -> Int {\n" + " return id\n" + "}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " func run() -> Int {\n" + " return fetch(\"https://api.example.com/data\")\n" + " }\n" + "}\n"); + for (int i = 0; i < 52; i++) { + char path[64], source[80]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(source, sizeof(source), "func filler%d() {}\n", i); + write_temp_file(tmp, path, source); + } + + char *previous_workers = getenv("CBM_WORKERS"); + char *saved_workers = previous_workers ? strdup(previous_workers) : NULL; + cbm_setenv("CBM_WORKERS", "4", 1); + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swiftincompat.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + int run_result = p ? cbm_pipeline_run(p) : -1; + if (saved_workers) { + cbm_setenv("CBM_WORKERS", saved_workers, 1); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + free(saved_workers); + ASSERT_NOT_NULL(p); + ASSERT_EQ(run_result, 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + cbm_node_t *fetches = NULL; + int fetch_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "fetch", &fetches, &fetch_count), + CBM_STORE_OK); + ASSERT_EQ(fetch_count, 1); + cbm_node_t *runs = NULL; + int run_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "run", &runs, &run_count), CBM_STORE_OK); + ASSERT_EQ(run_count, 1); + ASSERT_EQ(pipeline_has_calls_edge(s, runs[0].id, fetches[0].id), 0); + cbm_store_free_nodes(fetches, fetch_count); + cbm_store_free_nodes(runs, run_count); + + ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + /* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's * resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls * but forces the parallel resolver, since the empty-resolution fallback is a @@ -15384,6 +15510,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_swift_http_call_makes_route_issue1892); RUN_TEST(pipeline_swift_overloads_keep_argument_labels_issue2061); RUN_TEST(pipeline_swift_overloads_parallel_candidates_issue2061); + RUN_TEST(pipeline_swift_incompatible_overload_unresolved_issue2061); + RUN_TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061); RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls); RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http); /* Git history pass */ From bbd0957937ee01c483c04fe5d20f23f72bcd1fc5 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Wed, 30 Sep 2026 01:31:19 +0800 Subject: [PATCH 08/49] fix(pipeline): use tracked allocator for suffix lookup Signed-off-by: DavidHLP --- src/pipeline/registry.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index cd8f0c2180..1c96b4d07e 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -29,6 +29,7 @@ enum { REG_MAX_CANDIDATES = 256 }; #include "foundation/compat.h" /* CBM_TLS */ #include "foundation/hash_table.h" #include "foundation/dyn_array.h" +#include "foundation/mem_core.h" #include "foundation/platform.h" #include "callable_sig.h" /* cbm_qn_callable_base_len_named: overloads share the name key */ @@ -1735,14 +1736,14 @@ int cbm_registry_find_ending_with(const cbm_registry_t *r, const char *suffix, c /* Build ".suffix" target */ size_t slen = strlen(suffix); - char *target = malloc(slen + REG_SUFFIX_ALLOC); + char *target = cbm_alloc(CBM_MEM_CLASS_OTHER, slen + REG_SUFFIX_ALLOC); target[0] = '.'; memcpy(target + SKIP_ONE, suffix, slen + SKIP_ONE); struct few_ctx ctx = {target, slen + SKIP_ONE, NULL, 0, 0}; cbm_ht_foreach(r->exact, few_scan, &ctx); - free(target); + cbm_free(CBM_MEM_CLASS_OTHER, target); *out = ctx.results; return ctx.count; } From 210020492dcb0f8bd7296d9420adc663ef4e8d15 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Wed, 30 Sep 2026 09:56:11 +0800 Subject: [PATCH 09/49] fix: guard zero-length signature buffer copy Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 6e2c8b08e4..a49301d158 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -78,7 +78,9 @@ static void sig_raw(sig_ctx_t *c, const char *s, size_t n) { c->overflow = true; return; } - memcpy(grown, c->buf, c->len); + if (c->len != 0) { + memcpy(grown, c->buf, c->len); + } c->buf = grown; c->cap = cap; } From b0958c6cd36c68825fac55cc58934181a2251192 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Wed, 30 Sep 2026 11:02:41 +0800 Subject: [PATCH 10/49] refactor: narrow Swift candidate variable scope Signed-off-by: DavidHLP --- src/pipeline/registry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 1c96b4d07e..17803abb8e 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1557,8 +1557,8 @@ int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, } /* Preserve the receiver when it names a class or namespace explicitly. */ const char *chosen = NULL; - size_t callee_len = strlen(call->callee_name); if (strchr(call->callee_name, '.')) { + size_t callee_len = strlen(call->callee_name); for (int i = 0; i < matched; i++) { size_t base = cbm_qn_callable_base_len_named(matches[i], name); if (base >= callee_len && From f583ad901f26659ab6d46669292cc4fd1186f387 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 3 Oct 2026 09:32:33 +0800 Subject: [PATCH 11/49] fix(swift): match multiple trailing closure labels Swift-scope checkpoint for #2061: capture bounded trailing-closure labels, match Swift overload candidates conservatively, and cover extraction, registry, compact/spill, serial, parallel, and incremental behavior. Validation: ASan passed on the combined WIP source only; this A-only checkpoint has not been independently verified. GCC TSan, smoke, and memory gates remain failed/open. This commit is a scope checkpoint, not acceptance. Signed-off-by: David HLP --- internal/cbm/cbm.h | 15 ++ internal/cbm/extract_calls.c | 71 ++++++- internal/cbm/result_compact.c | 6 + src/pipeline/registry.c | 380 ++++++++++++++++++++++------------ tests/test_extraction.c | 133 ++++++++++++ tests/test_pipeline.c | 98 ++++++++- tests/test_registry.c | 143 +++++++++++++ 7 files changed, 711 insertions(+), 135 deletions(-) diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 999da30478..e4a12a5712 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -264,6 +264,9 @@ typedef struct { } CBMCallArg; #define CBM_MAX_CALL_ARGS 8 +/* Swift trailing-closure capture bound (#2061): more than this on one call + * marks the call truncated and the overload matcher fails closed. */ +#define CBM_MAX_TRAILING_CLOSURES 8 /* Byte offsets are meaningful only within the source buffer that produced * them. C/C++/CUDA run both raw and preprocessed extraction passes, and those @@ -311,6 +314,18 @@ typedef struct { // guard's unique-name exemption. Default false. bool swift_trailing_closure; bool swift_args_truncated; + /* Swift multi-trailing-closure capture (#2061). `swift_trailing_closure` + * stays the "there is at least one trailing closure" flag manual callers + * and existing tests set; `swift_trailing_count` is the actual number + * captured (0 => fall back to the bool, treated as one unlabelled closure). + * `swift_trailing_labels[i]` is closure i's argument label from the + * grammar's `lambda_literal (simple_identifier ':')*` sequence (NULL when + * unlabelled; entry 0 is always unlabelled). More closures than + * CBM_MAX_TRAILING_CLOSURES sets `swift_trailing_truncated`, and the + * overload matcher then fails closed rather than guessing a partial set. */ + uint8_t swift_trailing_count; + bool swift_trailing_truncated; + const char **swift_trailing_labels; } CBMCall; typedef struct { diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 161b2e4728..32c9ec0a33 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -3343,6 +3343,74 @@ static TSNode swift_call_args(TSNode node) { return cbm_find_child_by_kind(suffix, "value_arguments"); } +/* Store one bounded trailing-closure label; closure zero is unlabelled. */ +static void swift_store_trailing_label(CBMExtractCtx *ctx, CBMCall *call, uint32_t index, + const char *label, bool has_label) { + if (index == 0) { + call->swift_trailing_closure = true; + return; + } + if (index >= CBM_MAX_TRAILING_CLOSURES) { + call->swift_trailing_truncated = true; + return; + } + if (!call->swift_trailing_labels) { + size_t bytes = CBM_MAX_TRAILING_CLOSURES * sizeof(char *); + call->swift_trailing_labels = cbm_arena_calloc(ctx->arena, bytes); + } + if (!call->swift_trailing_labels) { + call->swift_trailing_truncated = true; + return; + } + call->swift_trailing_labels[index] = label; + if (has_label && !label) { + call->swift_trailing_truncated = true; + } +} + +/* Swift records trailing closures on the call_suffix: the hidden + * _fn_call_lambda_arguments rule inlines to + * `lambda_literal (simple_identifier ':')*`, so every closure literal, and the + * label that precedes all but the first, sit flat among the suffix's children + * (after an optional value_arguments). Each lambda_literal is a trailing + * closure; a simple_identifier directly before a closure is that closure's + * label. Closure 0 is always unlabelled. More closures than the bounded + * storage marks the call truncated so the overload matcher fails closed. */ +static void swift_capture_trailing_closures(CBMExtractCtx *ctx, TSNode call_node, TSNode suffix, + CBMCall *call) { + if (ts_node_has_error(call_node)) { + call->swift_trailing_truncated = true; + } + uint32_t children = ts_node_child_count(suffix); + uint32_t closures = 0; + const char *pending_label = NULL; + bool has_pending_label = false; + for (uint32_t i = 0; i < children; i++) { + TSNode child = ts_node_child(suffix, i); + const char *kind = ts_node_type(child); + if (ts_node_is_missing(child) || ts_node_has_error(child)) { + call->swift_trailing_truncated = true; + } + if (strcmp(kind, "lambda_literal") == 0) { + swift_store_trailing_label(ctx, call, closures, pending_label, has_pending_label); + closures++; + pending_label = NULL; + has_pending_label = false; + } else if (strcmp(kind, "simple_identifier") == 0) { + if (has_pending_label) { + call->swift_trailing_truncated = true; + } + pending_label = cbm_node_text(ctx->arena, child, ctx->source); + has_pending_label = true; + } + } + if (has_pending_label) { + call->swift_trailing_truncated = true; + } + call->swift_trailing_count = + (uint8_t)(closures > CBM_MAX_TRAILING_CLOSURES ? CBM_MAX_TRAILING_CLOSURES : closures); +} + static bool node_has_token(TSNode node, const char *token) { uint32_t count = ts_node_child_count(node); for (uint32_t i = 0; i < count; i++) { @@ -3963,8 +4031,7 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML if (ctx->language == CBM_LANG_SWIFT) { TSNode suffix = cbm_find_child_by_kind(node, "call_suffix"); if (!ts_node_is_null(suffix)) { - call.swift_trailing_closure = - !ts_node_is_null(cbm_find_child_by_kind(suffix, "lambda_literal")); + swift_capture_trailing_closures(ctx, node, suffix, &call); } uint32_t value_arg_count = 0; if (!ts_node_is_null(args)) { diff --git a/internal/cbm/result_compact.c b/internal/cbm/result_compact.c index 8fedf8269a..cad1780304 100644 --- a/internal/cbm/result_compact.c +++ b/internal/cbm/result_compact.c @@ -297,6 +297,12 @@ static void cr_walk_call(cr_ctx_t *c, CBMCall *call) { call->args = NULL; call->arg_count = 0; } + /* Swift trailing-closure labels are a bounded counted string list. */ + int labels = call->swift_trailing_count; + if (labels > CBM_MAX_TRAILING_CLOSURES) { + labels = CBM_MAX_TRAILING_CLOSURES; + } + cr_counted_list(c, &call->swift_trailing_labels, labels); } static void cr_walk(cr_ctx_t *c, CBMFileResult *r) { diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index a85fd1e883..d1ea3d414e 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -35,6 +35,7 @@ enum { REG_MAX_CANDIDATES = 256 }; #include #include +#include #include #include @@ -1520,114 +1521,285 @@ static bool swift_type_is_closure(const char *start, const char *end) { return false; } -static bool swift_signature_matches(const char *qn, const char *name, const swift_signature_t *meta, - const CBMCall *call) { - if (!meta || meta->count == UINT8_MAX || call->swift_args_truncated) { +enum { SWIFT_MAX_PARAMETERS = 64, SWIFT_INCOMPATIBLE = -1 }; + +typedef struct { + const char *labels[SWIFT_MAX_PARAMETERS]; + size_t label_lens[SWIFT_MAX_PARAMETERS]; + bool closures[SWIFT_MAX_PARAMETERS]; + bool variadics[SWIFT_MAX_PARAMETERS]; + unsigned count; +} swift_parameters_t; + +typedef struct { + int parens; + int brackets; + int angles; +} swift_parameter_depth_t; + +/* Exact match for a later trailing closure's label. NULL and empty labels + * never match, so a call with no captured label binds nothing by label. */ +static bool swift_label_matches(const char *label, const char *want, size_t want_len) { + return label && label[0] && strlen(label) == want_len && memcmp(label, want, want_len) == 0; +} + +/* Whether trailing closure used may bind signature parameter i. The first + * unlabelled closure skips a default or variadic parameter only if a required + * parameter precedes the first parameter matching the next trailing label. */ +static bool swift_trailing_binds(const swift_signature_t *meta, const CBMCall *call, + const swift_parameters_t *params, unsigned i, unsigned total, + unsigned used) { + if (used >= total || !params->closures[i]) { + return false; + } + if (used > 0) { + const char *tl = call->swift_trailing_labels ? call->swift_trailing_labels[used] : NULL; + return swift_label_matches(tl, params->labels[i], params->label_lens[i]); + } + if (!params->variadics[i] && (meta->defaults & (UINT64_C(1) << i)) == 0) { + return true; + } + const char *next_label = total > SKIP_ONE && call->swift_trailing_labels + ? call->swift_trailing_labels[SKIP_ONE] + : NULL; + for (unsigned j = i + SKIP_ONE; j < params->count; j++) { + if (swift_label_matches(next_label, params->labels[j], params->label_lens[j])) { + break; + } + if (!params->variadics[j] && (meta->defaults & (UINT64_C(1) << j)) == 0) { + return false; + } + } + return true; +} + +static bool swift_parameter_add(swift_parameters_t *params, const char *entry, const char *end) { + if (params->count >= SWIFT_MAX_PARAMETERS) { + return false; + } + const char *colon = memchr(entry, ':', (size_t)(end - entry)); + if (!colon) { return false; } + unsigned i = params->count; + params->labels[i] = entry; + params->label_lens[i] = (size_t)(colon - entry); + params->variadics[i] = end > colon + SKIP_ONE && end[-SKIP_ONE] == '~'; + const char *type_end = params->variadics[i] ? end - SKIP_ONE : end; + params->closures[i] = swift_type_is_closure(colon + SKIP_ONE, type_end); + params->count++; + return true; +} + +static void swift_parameter_depth_step(swift_parameter_depth_t *depth, char ch, char previous) { + switch (ch) { + case '(': + depth->parens++; + break; + case ')': + if (depth->parens > 0) { + depth->parens--; + } + break; + case '[': + depth->brackets++; + break; + case ']': + if (depth->brackets > 0) { + depth->brackets--; + } + break; + case '<': + depth->angles++; + break; + case '>': + if (depth->angles > 0 && previous != '=') { + depth->angles--; + } + break; + default: + break; + } +} + +/* Split only at top-level commas; preserve the closure/variadic spelling rules. */ +static bool swift_parameters_read(const char *qn, const char *name, const swift_signature_t *meta, + swift_parameters_t *params) { size_t base_len = cbm_qn_callable_base_len_named(qn, name); const char *sig = qn + base_len; size_t n = strlen(sig); - if (n < 2 || sig[0] != '(' || sig[n - 1] != ')' || strchr(sig, '#')) { + if (n < PAIR_LEN || sig[0] != '(' || sig[n - SKIP_ONE] != ')' || strchr(sig, '#')) { return false; } - const char *labels[64]; - size_t label_lens[64]; - bool closures[64]; - bool variadics[64]; - unsigned count = 0; - const char *entry = sig + 1; - const char *end = sig + n - 1; - int parens = 0, brackets = 0, angles = 0; + const char *entry = sig + SKIP_ONE; + const char *end = sig + n - SKIP_ONE; + swift_parameter_depth_t depth = {0}; for (const char *p = entry; p <= end; p++) { - char ch = *p; - if (p == end || (ch == ',' && parens == 0 && brackets == 0 && angles == 0)) { - if (p > entry) { - if (count >= 64) { - return false; - } - const char *colon = memchr(entry, ':', (size_t)(p - entry)); - if (!colon) { - return false; - } - labels[count] = entry; - label_lens[count] = (size_t)(colon - entry); - variadics[count] = p > colon + 1 && p[-1] == '~'; - const char *type_end = variadics[count] ? p - 1 : p; - closures[count] = swift_type_is_closure(colon + 1, type_end); - count++; + bool boundary = p == end || (*p == ',' && depth.parens == 0 && depth.brackets == 0 && + depth.angles == 0); + if (boundary) { + if (p > entry && !swift_parameter_add(params, entry, p)) { + return false; } - entry = p + 1; - } else if (ch == '(') { - parens++; - } else if (ch == ')' && parens > 0) { - parens--; - } else if (ch == '[') { - brackets++; - } else if (ch == ']' && brackets > 0) { - brackets--; - } else if (ch == '<') { - angles++; - } else if (ch == '>' && angles > 0 && (p == entry || p[-1] != '=')) { - angles--; + entry = p + SKIP_ONE; + continue; } + char previous = p == entry ? '\0' : p[-SKIP_ONE]; + swift_parameter_depth_step(&depth, *p, previous); } - if (count != meta->count) { + return params->count == meta->count; +} + +static bool swift_argument_label_matches(const swift_parameters_t *params, const CBMCall *call, + unsigned i, int arg) { + if (arg >= call->arg_count) { return false; } + const char *given = call->args[arg].keyword; + if (!given) { + given = "_"; + } + return strlen(given) == params->label_lens[i] && + memcmp(given, params->labels[i], params->label_lens[i]) == 0; +} + +static int swift_variadic_advance(const CBMCall *call, int arg, bool label_matches) { + if (!label_matches) { + return arg; + } + arg++; + while (arg < call->arg_count && !call->args[arg].keyword) { + arg++; + } + return arg; +} + +static bool swift_parameters_match(const swift_parameters_t *params, const swift_signature_t *meta, + const CBMCall *call) { + /* Preserve the legacy bool as one unlabelled closure for manual callers. */ + unsigned trailing_total = call->swift_trailing_count; + if (trailing_total == 0 && call->swift_trailing_closure) { + trailing_total = SKIP_ONE; + } + unsigned trailing_used = 0; int arg = 0; - bool used_closure = false; - for (unsigned i = 0; i < count; i++) { - const char *given = arg < call->arg_count ? call->args[arg].keyword : NULL; - if (!given) { - given = "_"; - } - bool label_matches = arg < call->arg_count && strlen(given) == label_lens[i] && - memcmp(given, labels[i], label_lens[i]) == 0; - bool later_required_closure = false; - if (call->swift_trailing_closure && closures[i]) { - for (unsigned j = i + 1; j < count; j++) { - if (closures[j] && !variadics[j] && - (meta->defaults & (UINT64_C(1) << j)) == 0) { - later_required_closure = true; - break; - } - } - } - if (variadics[i]) { - if (label_matches) { - arg++; - while (arg < call->arg_count && !call->args[arg].keyword) { - arg++; - } - } - if (call->swift_trailing_closure && !used_closure && closures[i] && - !later_required_closure) { - used_closure = true; + for (unsigned i = 0; i < params->count; i++) { + bool label_matches = swift_argument_label_matches(params, call, i, arg); + bool takes_trailing = + swift_trailing_binds(meta, call, params, i, trailing_total, trailing_used); + if (params->variadics[i]) { + arg = swift_variadic_advance(call, arg, label_matches); + if (takes_trailing && arg == call->arg_count) { + trailing_used++; } continue; } if (label_matches) { arg++; - } else if (call->swift_trailing_closure && !used_closure && arg == call->arg_count && - closures[i] && !later_required_closure) { - used_closure = true; + } else if (takes_trailing && arg == call->arg_count) { + trailing_used++; } else if ((meta->defaults & (UINT64_C(1) << i)) == 0) { return false; } } - return arg == call->arg_count && (!call->swift_trailing_closure || used_closure); + return arg == call->arg_count && trailing_used == trailing_total; +} + +static bool swift_signature_matches(const char *qn, const char *name, const swift_signature_t *meta, + const CBMCall *call) { + if (!meta || meta->count == UINT8_MAX || call->swift_args_truncated || + call->swift_trailing_truncated || call->swift_trailing_count > CBM_MAX_TRAILING_CLOSURES) { + return false; + } + swift_parameters_t params; + params.count = 0; + if (!swift_parameters_read(qn, name, meta, ¶ms)) { + return false; + } + return swift_parameters_match(¶ms, meta, call); +} + +/* Keep receiver choice ahead of general import/name choice. Without a module, + * this path still uses the deterministic distance tie-break, as before. */ +static const char *swift_choose_receiver(const char **matches, int matched, const char *name, + const CBMCall *call, const char *module_qn, + const char **import_vals, int import_count) { + if (!strchr(call->callee_name, '.')) { + return NULL; + } + size_t callee_len = strlen(call->callee_name); + const char *receiver_matches[REG_MAX_CANDIDATES]; + int receiver_count = 0; + for (int i = 0; i < matched; i++) { + size_t base = cbm_qn_callable_base_len_named(matches[i], name); + if (base >= callee_len && + memcmp(matches[i] + base - callee_len, call->callee_name, callee_len) == 0 && + (base == callee_len || matches[i][base - callee_len - SKIP_ONE] == '.')) { + receiver_matches[receiver_count++] = matches[i]; + } + } + if (receiver_count == 0) { + return NULL; + } + const char *reachable[REG_MAX_CANDIDATES]; + int reachable_count = 0; + if (import_vals) { + for (int i = 0; i < receiver_count; i++) { + if (is_import_reachable(receiver_matches[i], import_vals, import_count)) { + reachable[reachable_count++] = receiver_matches[i]; + } + } + } + const char **preferred = reachable_count > 0 ? reachable : receiver_matches; + int preferred_count = reachable_count > 0 ? reachable_count : receiver_count; + return best_by_import_distance(preferred, NULL, preferred_count, module_qn); +} + +static const char *swift_choose_candidate(const char **matches, int matched, const char *name, + const CBMCall *call, const char *module_qn, + const char **import_vals, int import_count) { + const char *chosen = + swift_choose_receiver(matches, matched, name, call, module_qn, import_vals, import_count); + if (chosen) { + return chosen; + } + const char *reachable[REG_MAX_CANDIDATES]; + int reachable_count = 0; + if (import_vals) { + for (int i = 0; i < matched; i++) { + if (is_import_reachable(matches[i], import_vals, import_count)) { + reachable[reachable_count++] = matches[i]; + } + } + } + if (reachable_count > 0) { + chosen = module_qn ? best_by_import_distance(reachable, NULL, reachable_count, module_qn) + : reachable[0]; + } + if (!chosen) { + chosen = + module_qn ? best_by_import_distance(matches, NULL, matched, module_qn) : matches[0]; + } + return chosen; } int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, const char *module_qn, const char **import_vals, int import_count, const char **out, int out_cap) { - if (!r || !r->swift_signatures || !call || !call->callee_name || !out || out_cap < 1) { + if (!r || !r->swift_signatures || !call || !call->callee_name || !out || out_cap < SKIP_ONE) { return 0; } const char *name = simple_name(call->callee_name); qn_array_t *bucket = cbm_ht_get(r->by_name, name); - if (!bucket || bucket->count > REG_MAX_CANDIDATES) { + if (!bucket) { + return 0; + } + if (bucket->count > REG_MAX_CANDIDATES) { + for (int i = 0; i < bucket->count; i++) { + if (cbm_ht_get(r->swift_signatures, bucket->items[i])) { + return SWIFT_INCOMPATIBLE; + } + } return 0; } const char *matches[REG_MAX_CANDIDATES]; @@ -1643,58 +1815,10 @@ int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, } } if (matched == 0) { - return has_swift ? -1 : 0; - } - /* Preserve an explicit receiver, but resolve duplicate receiver tails by - * module/import evidence and the registry's deterministic tie-break. */ - const char *chosen = NULL; - if (strchr(call->callee_name, '.')) { - size_t callee_len = strlen(call->callee_name); - const char *receiver_matches[REG_MAX_CANDIDATES]; - int receiver_count = 0; - for (int i = 0; i < matched; i++) { - size_t base = cbm_qn_callable_base_len_named(matches[i], name); - if (base >= callee_len && - memcmp(matches[i] + base - callee_len, call->callee_name, callee_len) == 0 && - (base == callee_len || matches[i][base - callee_len - 1] == '.')) { - receiver_matches[receiver_count++] = matches[i]; - } - } - if (receiver_count > 0) { - const char *reachable[REG_MAX_CANDIDATES]; - int reachable_count = 0; - if (import_vals) { - for (int i = 0; i < receiver_count; i++) { - if (is_import_reachable(receiver_matches[i], import_vals, import_count)) { - reachable[reachable_count++] = receiver_matches[i]; - } - } - } - const char **preferred = reachable_count > 0 ? reachable : receiver_matches; - int preferred_count = reachable_count > 0 ? reachable_count : receiver_count; - chosen = best_by_import_distance(preferred, NULL, preferred_count, module_qn); - } - } - if (!chosen) { - const char *reachable[REG_MAX_CANDIDATES]; - int reachable_count = 0; - if (import_vals) { - for (int i = 0; i < matched; i++) { - if (is_import_reachable(matches[i], import_vals, import_count)) { - reachable[reachable_count++] = matches[i]; - } - } - } - if (reachable_count > 0) { - chosen = module_qn - ? best_by_import_distance(reachable, NULL, reachable_count, module_qn) - : reachable[0]; - } - } - if (!chosen) { - chosen = - module_qn ? best_by_import_distance(matches, NULL, matched, module_qn) : matches[0]; + return has_swift ? SWIFT_INCOMPATIBLE : 0; } + const char *chosen = + swift_choose_candidate(matches, matched, name, call, module_qn, import_vals, import_count); if (!chosen) { return 0; } @@ -1704,7 +1828,7 @@ int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, size_t base = cbm_qn_callable_base_len_named(matches[i], name); if (base == chosen_base && memcmp(matches[i], chosen, base) == 0) { if (found == out_cap) { - return -1; + return SWIFT_INCOMPATIBLE; } out[found++] = matches[i]; } diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 5a21fe3e2e..7a61360d48 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -2058,6 +2058,46 @@ TEST(swift_method_call) { PASS(); } +TEST(swift_multiple_trailing_closure_extraction) { + CBMFileResult *r = extract( + "func outer() { service.handle { nested() } onError: { recover() } }\n" + "func one() { service.run { nested() } }\n" + "func incomplete() { service.handle { } onError: { recover( } }\n", + CBM_LANG_SWIFT, "t", "Calls.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + CBMCall *multi = NULL; + CBMCall *single = NULL; + CBMCall *incomplete = NULL; + for (int i = 0; i < r->calls.count; i++) { + CBMCall *call = &r->calls.items[i]; + if (call->callee_name && strcmp(call->callee_name, "service.handle") == 0) { + if (call->start_line == 1) { + multi = call; + } else { + incomplete = call; + } + } else if (call->callee_name && strcmp(call->callee_name, "service.run") == 0) { + single = call; + } + } + ASSERT_NOT_NULL(multi); + ASSERT_NOT_NULL(single); + ASSERT_NOT_NULL(incomplete); + ASSERT_EQ(multi->swift_trailing_count, 2); + ASSERT_TRUE(multi->swift_trailing_closure); + ASSERT_FALSE(multi->swift_trailing_truncated); + ASSERT_NOT_NULL(multi->swift_trailing_labels); + ASSERT_NULL(multi->swift_trailing_labels[0]); + ASSERT_STR_EQ(multi->swift_trailing_labels[1], "onError"); + ASSERT_EQ(single->swift_trailing_count, 1); + ASSERT_FALSE(single->swift_trailing_truncated); + ASSERT_EQ(incomplete->swift_trailing_count, 2); + ASSERT_TRUE(incomplete->swift_trailing_truncated); + cbm_free_result(r); + PASS(); +} + TEST(swift_constructor_call) { CBMFileResult *r = extract("func create() { let x = MyClass() }\n", CBM_LANG_SWIFT, "t", "create.swift"); @@ -8726,6 +8766,97 @@ TEST(extract_spill_round_trip_keeps_every_field) { PASS(); } +TEST(swift_trailing_closure_boundaries_survive_compact_and_spill) { + const char *src = "func eight() { service.eight { } first: { } second: { } third: { } " + "fourth: { } fifth: { } sixth: { } seventh: { } }\n" + "func nine() { service.nine { } first: { } second: { } third: { } " + "fourth: { } fifth: { } sixth: { } seventh: { } eighth: { } }\n"; + const char *callees[] = {"service.eight", "service.nine"}; + const char *labels[] = {NULL, "first", "second", "third", + "fourth", "fifth", "sixth", "seventh"}; + CBMFileResult *r = extract(src, CBM_LANG_SWIFT, "t", "TrailingBounds.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_NOT_NULL(r->cached_tree); + ASSERT_FALSE(ts_node_has_error(ts_tree_root_node(r->cached_tree))); + + char dir[512]; + snprintf(dir, sizeof(dir), "%s/cbm_spill_XXXXXX", cbm_tmpdir()); + ASSERT_NOT_NULL(cbm_mkdtemp(dir)); + cbm_result_spill_t *sp = cbm_result_spill_open(dir, 1, 1); + ASSERT_NOT_NULL(sp); + CBMFileResult *loaded[2] = {NULL, NULL}; + const CBMCall *loaded_calls[2][2] = {{NULL, NULL}, {NULL, NULL}}; + + /* Check real extraction, compaction, and both independently loaded copies. */ + for (int stage = 0; stage < 4; stage++) { + CBMFileResult *current = r; + if (stage == 1) { + cbm_result_compact(r); + } else if (stage >= 2) { + loaded[stage - 2] = cbm_result_spill_load(sp, 0); + current = loaded[stage - 2]; + } + ASSERT_NOT_NULL(current); + ASSERT_FALSE(current->has_error); + if (stage > 0) { + ASSERT_EQ(current->arena.nblocks, 1); + } + for (int c = 0; c < 2; c++) { + ASSERT_EQ(count_calls_named(current, callees[c]), 1); + const CBMCall *call = find_call_by_callee(current, callees[c]); + ASSERT_NOT_NULL(call); + ASSERT_EQ(call->swift_trailing_count, 8); + ASSERT_TRUE(call->swift_trailing_closure); + ASSERT_EQ(call->swift_trailing_truncated, c == 1); + ASSERT_NOT_NULL(call->swift_trailing_labels); + if (stage > 0) { + uintptr_t lo = (uintptr_t)current->arena.blocks[0]; + uintptr_t array = (uintptr_t)call->swift_trailing_labels; + size_t bytes = 8 * sizeof(*call->swift_trailing_labels); + ASSERT(current->arena.used >= bytes); + ASSERT(array >= lo && array - lo <= current->arena.used - bytes); + } + ASSERT_NULL(call->swift_trailing_labels[0]); + for (int i = 1; i < 8; i++) { + ASSERT_NOT_NULL(call->swift_trailing_labels[i]); + size_t label_bytes = strlen(labels[i]) + 1; + if (stage > 0) { + uintptr_t lo = (uintptr_t)current->arena.blocks[0]; + uintptr_t label = (uintptr_t)call->swift_trailing_labels[i]; + ASSERT(label >= lo && label - lo < current->arena.used); + ASSERT(label_bytes <= current->arena.used - (label - lo)); + } + ASSERT_MEM_EQ(call->swift_trailing_labels[i], labels[i], label_bytes); + } + if (stage >= 2) { + loaded_calls[stage - 2][c] = call; + } + } + if (stage == 1) { + ASSERT_TRUE(cbm_result_spill_park(sp, 0, 0, r)); + r = NULL; /* Park owns and frees the compacted result. */ + } + } + + ASSERT(loaded[0] != loaded[1]); + ASSERT(loaded[0]->arena.blocks[0] != loaded[1]->arena.blocks[0]); + for (int c = 0; c < 2; c++) { + ASSERT(loaded_calls[0][c]->swift_trailing_labels != + loaded_calls[1][c]->swift_trailing_labels); + for (int i = 1; i < 8; i++) { + ASSERT(loaded_calls[0][c]->swift_trailing_labels[i] != + loaded_calls[1][c]->swift_trailing_labels[i]); + } + } + + cbm_free_result(loaded[1]); + cbm_free_result(loaded[0]); + cbm_result_spill_close(sp); + cbm_rmdir(dir); + PASS(); +} + /* The low-disk guard, pinned through the free-space seam rather than the host * disk: one byte under the floor refuses (spilling onto a nearly full disk is * a worse failure than the memory pressure it relieves), the floor itself @@ -9004,6 +9135,8 @@ SUITE(extraction) { RUN_TEST(swift_struct); RUN_TEST(swift_simple_call); RUN_TEST(swift_method_call); + RUN_TEST(swift_multiple_trailing_closure_extraction); + RUN_TEST(swift_trailing_closure_boundaries_survive_compact_and_spill); RUN_TEST(swift_constructor_call); RUN_TEST(swift_chained_call); RUN_TEST(swift_force_unwrap_scanner_shift); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index e0a474fd4a..eaba09ebb9 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -7628,6 +7628,8 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { " func withDefault(message: String, flag: Bool = true) { sink.target() }\n" " func withCallback(completion: () -> Void) { sink.target() }\n" " func many(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, h: Int) {}\n" + " func handle(completion: () -> Void) { sink.target() }\n" + " func handle(completion: () -> Void, onError: () -> Void) { sink.target() }\n" "}\n"); write_temp_file(tmp, "Sources/Caller.swift", "class Caller {\n" @@ -7647,6 +7649,9 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { " self.service.many(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 // trailing comment\n" " )\n" " }\n" + " func callHandle() { self.service.handle { } onError: { } }\n" + " func callHandleWrongLabel() { self.service.handle { } bogus: { } }\n" + " func callHandleIncomplete() { self.service.handle { } onError: { recover( } }\n" "}\n"); char db_path[512]; @@ -7763,6 +7768,44 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { ASSERT_EQ(pipeline_has_calls_edge(s, record_caller.id, record.id), 1); ASSERT_EQ(pipeline_has_calls_edge(s, many_caller.id, many.id), 1); ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 0); + + /* #2061: two trailing closures select only the overload that can take + * them; a second closure whose label matches no parameter binds neither. */ + char handle_one_qn[512], handle_two_qn[512], handle_caller_qn[512], handle_wrong_qn[512], + handle_incomplete_qn[512]; + snprintf(handle_one_qn, sizeof(handle_one_qn), + "%s.Sources.Service.Service.handle(completion:()=>Void)", project); + snprintf(handle_two_qn, sizeof(handle_two_qn), + "%s.Sources.Service.Service.handle(completion:()=>Void,onError:()=>Void)", project); + snprintf(handle_caller_qn, sizeof(handle_caller_qn), + "%s.Sources.Caller.Caller.callHandle()", project); + snprintf(handle_wrong_qn, sizeof(handle_wrong_qn), + "%s.Sources.Caller.Caller.callHandleWrongLabel()", project); + snprintf(handle_incomplete_qn, sizeof(handle_incomplete_qn), + "%s.Sources.Caller.Caller.callHandleIncomplete()", project); + cbm_node_t handle_one = {0}, handle_two = {0}, handle_caller = {0}; + cbm_node_t handle_wrong_caller = {0}, handle_incomplete_caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_one_qn, &handle_one), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_two_qn, &handle_two), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_caller_qn, &handle_caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_wrong_qn, &handle_wrong_caller), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_incomplete_qn, &handle_incomplete_caller), + CBM_STORE_OK); + /* `handle { } onError: { }`: only the two-closure overload can take both. */ + ASSERT_EQ(pipeline_has_calls_edge(s, handle_caller.id, handle_two.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_caller.id, handle_one.id), 0); + /* A second closure whose label matches no parameter binds neither. */ + ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong_caller.id, handle_two.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong_caller.id, handle_one.id), 0); + /* An incomplete second closure marks candidate input unknown; resolver must not emit a bare-name edge. */ + ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete_caller.id, handle_two.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete_caller.id, handle_one.id), 0); + cbm_node_free_fields(&handle_incomplete_caller); + cbm_node_free_fields(&handle_wrong_caller); + cbm_node_free_fields(&handle_one); + cbm_node_free_fields(&handle_two); + cbm_node_free_fields(&handle_caller); cbm_node_free_fields(&record); cbm_node_free_fields(&record_caller); cbm_node_free_fields(&many); @@ -7795,6 +7838,8 @@ TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { " func upload(_ data: Data, to url: URL) {}\n" " func upload(_ file: URL, to url: URL) {}\n" " func record(path: String, session: URLSession) {}\n" + " func handle(completion: () -> Void) {}\n" + " func handle(completion: () -> Void, onError: () -> Void) {}\n" "}\n"); write_temp_file(tmp, "Sources/Caller.swift", "class Caller {\n" @@ -7803,6 +7848,9 @@ TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { " self.service.upload(Data(), to: URL(string: \"/x\")!)\n" " self.service.record(path: \"/audit\", session: URLSession.shared)\n" " }\n" + " func callHandle() { self.service.handle { } onError: { } }\n" + " func callHandleWrongLabel() { self.service.handle { } bogus: { } }\n" + " func callHandleIncomplete() { self.service.handle { } onError: { recover( } }\n" "}\n"); for (int i = 0; i < 50; i++) { char path[64], source[80]; @@ -7859,6 +7907,39 @@ TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { } ASSERT_EQ(ambiguous, 2); cbm_store_free_edges(edges, edge_count); + + /* #2061: the parallel resolver must reach the same trailing-closure + * verdicts as the serial one. */ + char handle_one_qn[512], handle_two_qn[512], handle_qn[512], handle_wrong_qn[512], + handle_incomplete_qn[512]; + snprintf(handle_one_qn, sizeof(handle_one_qn), + "%s.Sources.Service.Service.handle(completion:()=>Void)", project); + snprintf(handle_two_qn, sizeof(handle_two_qn), + "%s.Sources.Service.Service.handle(completion:()=>Void,onError:()=>Void)", project); + snprintf(handle_qn, sizeof(handle_qn), "%s.Sources.Caller.Caller.callHandle()", project); + snprintf(handle_wrong_qn, sizeof(handle_wrong_qn), + "%s.Sources.Caller.Caller.callHandleWrongLabel()", project); + snprintf(handle_incomplete_qn, sizeof(handle_incomplete_qn), + "%s.Sources.Caller.Caller.callHandleIncomplete()", project); + cbm_node_t handle_one = {0}, handle_two = {0}, handle = {0}; + cbm_node_t handle_wrong = {0}, handle_incomplete = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_one_qn, &handle_one), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_two_qn, &handle_two), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_qn, &handle), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_wrong_qn, &handle_wrong), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_incomplete_qn, &handle_incomplete), + CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(s, handle.id, handle_two.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, handle.id, handle_one.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong.id, handle_two.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong.id, handle_one.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete.id, handle_two.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete.id, handle_one.id), 0); + cbm_node_free_fields(&handle_incomplete); + cbm_node_free_fields(&handle_wrong); + cbm_node_free_fields(&handle_one); + cbm_node_free_fields(&handle_two); + cbm_node_free_fields(&handle); cbm_node_free_fields(&caller); cbm_node_free_fields(&data); cbm_node_free_fields(&url); @@ -16657,8 +16738,9 @@ TEST(pipeline_swift_incremental_restores_default_signature_issue2061) { " func good() { self.service.ping(a: 1) }\n" " func bad() { self.service.ping(x: 1) }\n" "}\n"); - char db_path[512], caller_path[512], project[512], service_path[512]; + char db_path[512], closure_db_path[512], caller_path[512], project[512], service_path[512]; snprintf(db_path, sizeof(db_path), "%s/swift-restore.db", tmp); + snprintf(closure_db_path, sizeof(closure_db_path), "%s/swift-closure.db", tmp); snprintf(caller_path, sizeof(caller_path), "%s/Sources/Caller.swift", tmp); snprintf(service_path, sizeof(service_path), "%s/Sources/Service.swift", tmp); @@ -16691,17 +16773,23 @@ TEST(pipeline_swift_incremental_restores_default_signature_issue2061) { cbm_store_close(store); cbm_pipeline_free(legacy); cbm_pipeline_incremental_test_reset_faults(); + /* The test-only legacy partial route predates persisted LSP surfaces. + * Seed a separate full generation for closure-repair assertions. */ + cbm_pipeline_t *closure_seed = cbm_pipeline_new(tmp, closure_db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(closure_seed); + ASSERT_EQ(cbm_pipeline_run(closure_seed), 0); + cbm_pipeline_free(closure_seed); caller = fopen(caller_path, "ab"); ASSERT_NOT_NULL(caller); ASSERT_TRUE(fputs("// closure marker\n", caller) >= 0); ASSERT_EQ(fclose(caller), 0); ASSERT_EQ(pipeline_test_set_mtime(caller_path, 2000000001, 0), 0); - cbm_pipeline_t *closure = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + cbm_pipeline_t *closure = cbm_pipeline_new(tmp, closure_db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(closure); ASSERT_EQ(cbm_pipeline_run(closure), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); - store = cbm_store_open_path(db_path); + store = cbm_store_open_path(closure_db_path); ASSERT_NOT_NULL(store); ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, true)); cbm_store_close(store); @@ -16713,11 +16801,11 @@ TEST(pipeline_swift_incremental_restores_default_signature_issue2061) { " func other(a: Int, b: Int = 0) {}\n" "}\n"); ASSERT_EQ(pipeline_test_set_mtime(service_path, 2000000002, 0), 0); - cbm_pipeline_t *defaults_removed = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + cbm_pipeline_t *defaults_removed = cbm_pipeline_new(tmp, closure_db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(defaults_removed); ASSERT_EQ(cbm_pipeline_run(defaults_removed), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); - store = cbm_store_open_path(db_path); + store = cbm_store_open_path(closure_db_path); ASSERT_NOT_NULL(store); ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, false)); cbm_store_close(store); diff --git a/tests/test_registry.c b/tests/test_registry.c index 92ac8022e4..70ed8456b5 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1388,6 +1388,144 @@ TEST(swift_receiver_resolution_is_independent_of_registration_order) { PASS(); } +/* Two trailing closures bind positionally: the first (unlabelled) to the + * first free closure parameter, later ones by their exact label (#2061). + * `handle { } onError: { }` can only be the two-closure overload; a wrong + * second label matches neither overload. */ +TEST(swift_multiple_trailing_closures_match_labels) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + const char *one_qn = "proj.Service.handle(completion:()=>Void)"; + const char *two_qn = "proj.Service.handle(completion:()=>Void,onError:()=>Void)"; + cbm_registry_add(r, "handle", one_qn, "Method"); + cbm_registry_add(r, "handle", two_qn, "Method"); + cbm_registry_set_swift_signature(r, one_qn, 0, 1); + cbm_registry_set_swift_signature(r, two_qn, 0, 2); + + const char *good[2] = {NULL, "onError"}; + CBMCall call = {.callee_name = "Service.handle", + .swift_trailing_closure = true, + .swift_trailing_count = 2, + .swift_trailing_labels = good}; + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], two_qn); + + /* A second closure whose label matches no parameter matches nothing. */ + const char *bad[2] = {NULL, "unknown"}; + call.swift_trailing_labels = bad; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + + /* Two closures cannot fit the one-closure overload even with no labels. */ + const char *anon[2] = {NULL, NULL}; + call.swift_trailing_labels = anon; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + + call.swift_trailing_count = 1; + call.swift_trailing_truncated = true; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + call.swift_trailing_count = 2; + call.swift_trailing_truncated = false; + + /* A later label satisfies its required closure, so the first unlabelled + * closure can skip an earlier defaulted closure and bind the required one. */ + const char *delayed_qn = + "proj.Service.delayed(onDismiss:(()=>Void)?,content:()=>Void,onError:(()=>Void)?)"; + cbm_registry_add(r, "delayed", delayed_qn, "Method"); + cbm_registry_set_swift_signature(r, delayed_qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 2), 3); + call.callee_name = "Service.delayed"; + call.swift_trailing_labels = good; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], delayed_qn); + + cbm_registry_free(r); + PASS(); +} + +/* Repeated external labels must stop the default-skip lookahead at the + * next trailing label, even if a still later closure has the same label. */ +TEST(swift_repeated_labels_skip_default_before_next_trailing_label) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + const char *qn = "proj.Service.repeated(a:()=>Void,b:()=>Void,a:()=>Void,b:()=>Void)"; + cbm_registry_add(r, "repeated", qn, "Method"); + cbm_registry_set_swift_signature(r, qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 3), 4); + const char *labels[3] = {NULL, "a", "b"}; + CBMCall call = {.callee_name = "Service.repeated", + .swift_trailing_closure = true, + .swift_trailing_count = 3, + .swift_trailing_labels = labels}; + const char *out[4] = {0}; + /* Parameter 1 is required before the next label at parameter 2, so + * parameter 0 uses its default and the closures bind parameters 1-3. */ + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], qn); + cbm_registry_free(r); + PASS(); +} + +TEST(swift_repeated_labels_do_not_skip_default_past_next_trailing_label) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + const char *qn = "proj.Service.repeated(a:()=>Void,b:()=>Void,a:()=>Void,b:()=>Void)"; + cbm_registry_add(r, "repeated", qn, "Method"); + cbm_registry_set_swift_signature(r, qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 1), 4); + const char *labels[2] = {NULL, "b"}; + CBMCall call = {.callee_name = "Service.repeated", + .swift_trailing_closure = true, + .swift_trailing_count = 2, + .swift_trailing_labels = labels}; + const char *out[4] = {0}; + /* The next label matches parameter 1 immediately. The first closure + * binds parameter 0, leaving required parameters 2 and 3 unsupplied. */ + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + cbm_registry_free(r); + PASS(); +} + +/* A variadic closure parameter already skipped by the parenthesized + * flag argument cannot consume an earlier trailing closure. */ +TEST(swift_variadic_trailing_closures_do_not_precede_parenthesized_argument) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + const char *qn = "proj.Service.f(callbacks:(()=>Void)~,flag:Bool,done:()=>Void)"; + cbm_registry_add(r, "f", qn, "Method"); + cbm_registry_set_swift_signature(r, qn, UINT64_C(1) << 1, 3); + CBMCallArg arg = {.keyword = "flag"}; + const char *labels[2] = {NULL, "done"}; + CBMCall call = {.callee_name = "Service.f", + .args = &arg, + .arg_count = 1, + .swift_trailing_closure = true, + .swift_trailing_count = 2, + .swift_trailing_labels = labels}; + const char *out[4] = {0}; + /* f(flag: true) { } done: { } cannot fill callbacks after flag was + * supplied. Only the done parameter remains for two trailing closures. */ + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + cbm_registry_free(r); + PASS(); +} + +TEST(swift_overloaded_bucket_fails_closed) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + const char *first_qn = "proj.Service.work(flag:Bool)"; + cbm_registry_add(r, "work", first_qn, "Method"); + cbm_registry_set_swift_signature(r, first_qn, 0, 1); + cbm_registry_add(r, "work", "proj.Other.work()", "Method"); + char qns[256][64]; + for (int i = 1; i < 256; i++) { + snprintf(qns[i], sizeof(qns[i]), "proj.Other%d.work()", i); + cbm_registry_add(r, "work", qns[i], "Method"); + } + CBMCall call = {.callee_name = "Service.work"}; + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + cbm_registry_free(r); + PASS(); +} + SUITE(registry) { /* FQN */ RUN_TEST(fqn_simple); @@ -1427,6 +1565,11 @@ SUITE(registry) { RUN_TEST(resolve_import_map_alias_with_suffix_hits_method); RUN_TEST(swift_receiver_resolution_is_independent_of_registration_order); RUN_TEST(swift_overload_labels_defaults_and_trailing_closure); + RUN_TEST(swift_multiple_trailing_closures_match_labels); + RUN_TEST(swift_repeated_labels_skip_default_before_next_trailing_label); + RUN_TEST(swift_repeated_labels_do_not_skip_default_past_next_trailing_label); + RUN_TEST(swift_variadic_trailing_closures_do_not_precede_parenthesized_argument); + RUN_TEST(swift_overloaded_bucket_fails_closed); RUN_TEST(resolve_unique_name); RUN_TEST(resolve_unresolved); RUN_TEST(resolve_many_nodes); From 73f4475f50b4466fc860b0d76c933281f4914bf8 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 3 Oct 2026 10:49:56 +0800 Subject: [PATCH 12/49] docs(swift): record focused identity validation evidence Signed-off-by: David HLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 88 +++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/SWIFT_IDENTITY_VALIDATION.md diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md new file mode 100644 index 0000000000..0af7a9fb9c --- /dev/null +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -0,0 +1,88 @@ +# Swift identity validation + +This records focused local evidence for issue #2061 and PR #2436: Swift +callable identity and conservative overload candidates, including candidate +counts and multiple trailing closure labels. The scope remains one language, +one claim. + +## Tested source + +The A-only checkpoint contains the seven Swift implementation and regression +test paths committed in `fix(swift): match multiple trailing closure labels`. +It was tested from a fresh `git archive` snapshot, without uncommitted +infrastructure changes or Git metadata. + +Public PR head S is `b0958c6cd36c68825fac55cc58934181a2251192`; it does +not contain this local A checkpoint. A's parent is local merge +`b7c86690618ed0accf137a42a987887872475fc3`. This document and the local +checkpoint code have not been pushed. PR readiness remains unestablished. + +| Identity | Value | +| --- | --- | +| Commit | `761ea8ca432ae0f58a6bb77c438ccedcb1ade3a1` | +| Tree | `b1718c8b0e14eb1bd7d2a2f6da1bda024e4a1912` | +| Archive SHA-256 | `d934685cc5e521060a8ddb29dcb65e5961c10d103763375e647a9e9da6a1617e` | +| Content manifest SHA-256 | `10011442026e4c5a4900c6b24ee19dda1ea3c1eda15e8bf88b0eec35b4ef77de` | + +The archive path set matched all 2,254 tracked paths, including one symlink. +The content manifest contains sorted file SHA-256 records and symlink targets. +Its before/after comparison returned **0**, with identical hashes. + +## Focused result + +From the archived source root, with `VALIDATION_BUILD_DIR` set to a separate +temporary build directory: + +```bash +scripts/test.sh --suites extraction,callable_sig,registry,pipeline \ + BUILD_DIR="$VALIDATION_BUILD_DIR" +``` + +The canonical iteration entry compiled the standard test runner and executed +exactly these four suites: + +| Suite | Relevant coverage | +| --- | --- | +| `extraction` | Multiple trailing closures, bounded labels, compact/spill round trips | +| `callable_sig` | Swift signature identity, defaults, identity length bounds | +| `registry` | Conservative overload candidates, closure labels, defaults and registration order | +| `pipeline` | Serial/parallel candidates and incremental signature restoration | + +Environment: Linux x86_64, GCC/G++ **16.2.1 20260810**, eight build cores. +The repository's default ASan+UBSan settings and the +`sanitized=1 test_seams=1` build-config assertion remained enabled. + +- Started: **2026-10-03T01:45:48Z**. +- Finished: **2026-10-03T02:12:28Z**. +- Test command exit: **0**. +- Complete runner summary: **787 passed**. +- Archived source content remained identical after execution. + +## Evidence limits and earlier failures + +No attributable raw Swift negative reproduction log was found for parent +baseline `5538355530bb126c3041f7fcf8ef7a82b4bb3fec`. This evidence therefore +does not establish a red/green comparison against that parent. + +Earlier checks used combined working trees containing Swift and independent +infrastructure changes, based on +`b7c86690618ed0accf137a42a987887872475fc3`. Their recorded +`git diff --binary` SHA-256 scopes differ: + +- GCC TSan and daemon smoke: + `b1c2b8cb54ab868ab7f053f1936ecc48370f7cd3123cb5f5efd23858a114982c`. +- The 131-source analyzer run, before the parser patch: + `c0ac0734e190a79031c5a430e4cd71f52e6280bdfbd47bf9631cfcb8d397c1be`. + +These results apply to their respective snapshots and were not rerun on the +A-only archive: + +| Earlier check | Recorded result and scope | +| --- | --- | +| GCC TSan | Canonical full TSan leg failed: exit 2; 1,220 passed, 2 failed, 8 skipped. Daemon frontend EOF fixtures hit the existing 90-second child alarm. | +| Daemon smoke | Production daemon/standalone CLI smoke failed: exit 2; standalone CLI created a daemon socket, violating the smoke test's one-shot expectation. | +| Memory analysis | Canonical 131-source analysis remained FAIL. Verified execution reported findings; earlier provisional green results with missing-tool/parser failure risks were invalid. Bounded LLVM 21/22 comparisons over seven translation units found the same 16 diagnostic lines across parent/local/combined snapshots; that diagnostic comparison does not replace the 131-source gate. | + +The focused A-only result does not establish full acceptance or PR readiness. +The earlier failures remain open in their respective scopes. No broader CLI, +daemon, Cypher, MCP or Store repair is included in this Swift checkpoint. From 6fc5af9a864c965b7304b5a4196510df4aa8b0ee Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 3 Oct 2026 12:25:29 +0800 Subject: [PATCH 13/49] test(swift): add standalone issue 2061 graph reproduction Signed-off-by: David HLP --- .../repro/issue2061_swift_identity/README.md | 150 +++++++++++++ tests/repro/issue2061_swift_identity/main.c | 198 ++++++++++++++++++ .../repro/issue2061_swift_identity/syntax.mk | 5 + 3 files changed, 353 insertions(+) create mode 100644 tests/repro/issue2061_swift_identity/README.md create mode 100644 tests/repro/issue2061_swift_identity/main.c create mode 100644 tests/repro/issue2061_swift_identity/syntax.mk diff --git a/tests/repro/issue2061_swift_identity/README.md b/tests/repro/issue2061_swift_identity/README.md new file mode 100644 index 0000000000..a03c5ba60e --- /dev/null +++ b/tests/repro/issue2061_swift_identity/README.md @@ -0,0 +1,150 @@ +# Original Swift issue #2061: standalone pipeline/store proof + +This runner embeds only the three original Swift files from +[issue #2061](https://github.com/DeusData/codebase-memory-mcp/issues/2061). +It uses the real production pipeline in FAST mode and queries its persisted +store. It neither implements a resolver nor copies a new resolver into P. +It is deliberately absent from `ALL_TEST_SRCS`, `TEST_REPRO_SRCS` and CI. + +**Review this checkpoint on GitHub before building or running either side.** +Syntax checks do not establish RED/GREEN. This is a focused graph proof, +not a full gate or a test of the `trace_path` presentation layer. + +The runner prints every observation and a final summary. It checks two +`work` nodes, their original file/line ranges, flag → target, name ↛ target, +caller → name, caller ↛ flag, and absence of the caller in target's inbound +CALLS traversal through depth three. Node lookup uses names, files and lines, +so it works with both P's unsuffixed QNs and A's suffixed QNs. Missing +endpoints produce unavailable edge observations (`-1`), never successful +absence checks. All semantic checks are collected rather than failing early. + +Exit meanings: **0** = all observations pass; **1** = semantic mismatches +after successful indexing/store access; **2** = setup, pipeline or store API +failure. A compiler/linker failure, timeout, signal, or sanitizer diagnostic +is an infrastructure/runtime failure, not the target RED. Only an exit-1 +run with the expected collapse/false-caller observations and no runtime +diagnostics establishes the baseline reproduction. + +## Syntax only (before review) + +From each exact source archive, using the same external checkpoint files: + +```bash +make -f "$harness/syntax.mk" issue2061-syntax \ + CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$build_dir" \ + ALL_TEST_SRCS="$harness/main.c" +``` + +`syntax.mk` only includes the archive's unmodified `Makefile.cbm` and adds +one `-fsyntax-only` recipe using its standard `CFLAGS_TEST`. It builds no +objects, links no executable and runs no fixture. It does not change flags. + +## Comparable P/A build and run (after root review) + +Run in Bash from the repository root. Set `reviewed_checkpoint` to the full +SHA root actually reviewed. Use one checkpoint's harness for both sides. +All archives, binaries, logs and generated fixtures stay outside Git. +No worktree or tracked source modification is required. + +```bash +set -euo pipefail +reviewed_checkpoint=REPLACE_WITH_ROOT_REVIEWED_FULL_SHA +evidence=$(mktemp -d /tmp/dav64-issue2061-XXXXXX) +mkdir "$evidence/harness" +git archive "$reviewed_checkpoint" tests/repro/issue2061_swift_identity \ + | tar -x -C "$evidence/harness" +harness="$evidence/harness/tests/repro/issue2061_swift_identity" +P=5538355530bb126c3041f7fcf8ef7a82b4bb3fec +A=cf67f49dc2d718709846adefff5bab6cf9b671d4 +gcc --version > "$evidence/toolchain.txt" +g++ --version >> "$evidence/toolchain.txt" +uname -a > "$evidence/platform.txt" +sha256sum "$harness/main.c" "$harness/syntax.mk" > "$evidence/harness.sha256" + +for side in P A; do + sha=${!side} + mkdir "$evidence/$side-source" + git archive "$sha" > "$evidence/$side.tar" + tar -xf "$evidence/$side.tar" -C "$evidence/$side-source" + printf '%s\n' "$sha" > "$evidence/$side.commit" + sha256sum "$evidence/$side.tar" > "$evidence/$side.archive.sha256" + build_dir="$evidence/$side-build" + ( + cd "$evidence/$side-source" + make -n -f Makefile.cbm "$build_dir/test-runner" \ + CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$build_dir" \ + ALL_TEST_SRCS="$harness/main.c" + ) > "$evidence/$side.build-plan.txt" 2>&1 +done +``` + +Before compiling, inspect both plans: the only test source must be the +external `main.c`; compiler, effective flags and dependency source sets must +be comparable (normalize the P/A source/build paths). Stop and report any +unexplained difference. The standard target name `test-runner` is retained, +but `ALL_TEST_SRCS` replaces the entire test source list with this one main. +The archive's standard production/extraction/grammar dependencies and +sanitizer flags are retained. No all-tests runner or suite is compiled/run. + +Then build **only the binary target**, never `test`, `test-focused` or +`test-repro`. For each side, record UTC start/end and the actual build exit: + +```bash +for side in P A; do + build_dir="$evidence/$side-build" + date -u +%FT%TZ > "$evidence/$side.build-start" + set +e + ( + cd "$evidence/$side-source" + make -j8 -f Makefile.cbm "$build_dir/test-runner" \ + CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$build_dir" \ + ALL_TEST_SRCS="$harness/main.c" + ) > "$evidence/$side.build.stdout" 2> "$evidence/$side.build.stderr" + rc=$? + set -e + printf '%s\n' "$rc" > "$evidence/$side.build.exit" + date -u +%FT%TZ > "$evidence/$side.build-end" + if [ "$rc" -ne 0 ]; then + printf 'BUILD FAILURE on %s; not semantic RED\n' "$side" + exit "$rc" + fi + sha256sum "$build_dir/test-runner" > "$evidence/$side.binary.sha256" +done +``` + +Use the same inherited environment for both runs. Record an allowlist of +relevant variables (compiler paths, sanitizer options, `CBM_*` resource/index +settings and locale), not a raw environment dump that might contain secrets. +Leave resolver controls enabled; do not disable LSP or alter graph behavior +to force a desired result. Use fresh per-side repositories/databases and +the same working directory. The run directory must not already exist. + +```bash +for side in P A; do + date -u +%FT%TZ > "$evidence/$side.run-start" + set +e + ( + cd "$evidence" + ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ + UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ + timeout 180s "$evidence/$side-build/test-runner" "$evidence/$side-run" + ) > "$evidence/$side.run.stdout" 2> "$evidence/$side.run.stderr" + rc=$? + set -e + printf '%s\n' "$rc" > "$evidence/$side.run.exit" + date -u +%FT%TZ > "$evidence/$side.run-end" +done +sha256sum "$harness/main.c" "$harness/syntax.mk" > "$evidence/harness-after.sha256" +cmp "$evidence/harness.sha256" "$evidence/harness-after.sha256" +for side in P A; do + sha256sum "$evidence/$side-run/repo/Sources/"*.swift \ + > "$evidence/$side.fixture.sha256" +done +``` + +Compare fixture bytes between sides and with the issue, and verify archived +source manifests before/after execution. Retain raw stdout/stderr, actual +exits, timestamps, expanded build plans, toolchain/platform and fingerprints. +Inspect all observations and sanitizer output before attributing RED/GREEN. +The three-file graph has no dependency on a daemon or a new CLI architecture. +No P/A execution result is claimed by this checkpoint. diff --git a/tests/repro/issue2061_swift_identity/main.c b/tests/repro/issue2061_swift_identity/main.c new file mode 100644 index 0000000000..63435c3c6d --- /dev/null +++ b/tests/repro/issue2061_swift_identity/main.c @@ -0,0 +1,198 @@ +/* Standalone original #2061 fixture; real pipeline/store, no resolver substitute. + * Exit 0: all observations pass; 1: semantic mismatch; 2: setup/API error. + * Run only after review. Retain the temporary repository and DB as evidence. + */ +#include "pipeline/pipeline.h" +#include "store/store.h" + +#include +#include +#include +#include + +static int passed, failed, errors; + +static void observe(const char *name, int actual, int expected) { + printf("OBS %s actual=%d expected=%d %s\n", name, actual, expected, + actual == expected ? "PASS" : "FAIL"); + if (actual == expected) + passed++; + else + failed++; +} + +static int api_ok(const char *name, int rc) { + if (rc == CBM_STORE_OK) + return 1; + fprintf(stderr, "ERROR %s rc=%d\n", name, rc); + errors++; + return 0; +} + +static int write_source(const char *root, const char *name, const char *source) { + char path[1024]; + if (snprintf(path, sizeof(path), "%s/repo/Sources/%s", root, name) >= (int)sizeof(path)) + return 0; + FILE *f = fopen(path, "wx"); + if (!f) + return 0; + int ok = fputs(source, f) >= 0; + if (fclose(f) != 0) + ok = 0; + return ok; +} + +/* File/line identity works on P's unsuffixed and A's suffixed QNs alike. */ +static int64_t find_node(cbm_store_t *s, const char *project, const char *name, const char *file, + int first, int last, int *total) { + cbm_node_t *nodes = NULL; + int count = 0, matches = 0; + int64_t id = 0; + if (!api_ok("find_nodes_by_name", + cbm_store_find_nodes_by_name(s, project, name, &nodes, &count))) + return 0; + if (total) + *total = count; + for (int i = 0; i < count; i++) { + const cbm_node_t *n = &nodes[i]; + printf("NODE name=%s id=%lld file=%s lines=%d-%d qn=%s\n", name, (long long)n->id, + n->file_path ? n->file_path : "", n->start_line, n->end_line, + n->qualified_name ? n->qualified_name : ""); + if (n->file_path && strcmp(n->file_path, file) == 0 && n->start_line == first && + n->end_line == last) { + id = n->id; + matches++; + } + } + cbm_store_free_nodes(nodes, count); + return matches == 1 ? id : 0; +} + +/* Missing endpoints are unavailable (-1), never evidence that an edge is absent. */ +static int calls(cbm_store_t *s, int64_t source, int64_t target) { + if (!source || !target) + return -1; + cbm_edge_t *edges = NULL; + int count = 0, found = 0; + if (!api_ok("find_edges_by_source_type", + cbm_store_find_edges_by_source_type(s, source, "CALLS", &edges, &count))) + return -1; + for (int i = 0; i < count; i++) + if (edges[i].target_id == target) + found = 1; + cbm_store_free_edges(edges, count); + return found; +} + +/* Traverse stored CALLS only, for the issue's inbound depth=3. Depth bounds + * cycles; no candidate cap can silently discard the offending caller. + * This checks graph reachability, not the trace_path presentation layer. + */ +static int inbound(cbm_store_t *s, int64_t target, int64_t caller, int depth) { + if (!target || !caller) + return -1; + if (!depth) + return 0; + cbm_edge_t *edges = NULL; + int count = 0, found = 0; + if (!api_ok("find_edges_by_target_type", + cbm_store_find_edges_by_target_type(s, target, "CALLS", &edges, &count))) + return -1; + for (int i = 0; i < count; i++) { + printf("INBOUND remaining_depth=%d source=%lld target=%lld\n", depth, + (long long)edges[i].source_id, (long long)target); + if (edges[i].source_id == caller) + found = 1; + int nested = inbound(s, edges[i].source_id, caller, depth - 1); + if (nested < 0) + found = -1; + else if (nested && found >= 0) + found = 1; + } + cbm_store_free_edges(edges, count); + return found; +} + +int main(int argc, char **argv) { + if (argc != 2) { + fprintf(stderr, "usage: %s /absolute/fresh-evidence-directory\n", argv[0]); + return 2; + } + char repo[1024], sources[1024], db[1024]; + if (argv[1][0] != '/' || + snprintf(repo, sizeof(repo), "%s/repo", argv[1]) >= (int)sizeof(repo) || + snprintf(sources, sizeof(sources), "%s/repo/Sources", argv[1]) >= (int)sizeof(sources) || + snprintf(db, sizeof(db), "%s/graph.db", argv[1]) >= (int)sizeof(db) || + mkdir(argv[1], 0700) != 0 || mkdir(repo, 0700) != 0 || mkdir(sources, 0700) != 0) { + perror("fresh evidence directories"); + return 2; + } + /* Verbatim Swift code blocks from github.com/DeusData/codebase-memory-mcp/issues/2061. */ + if (!write_source(argv[1], "Sink.swift", "class Sink {\n\tfunc target() {}\n}\n") || + !write_source(argv[1], "Service.swift", + "class Service {\n" + "\tlet sink = Sink()\n\n" + "\t// Overload A: DOES call target()\n" + "\tfunc work(flag: Bool) {\n" + "\t\tself.sink.target()\n\t}\n\n" + "\t// Overload B: does NOT call target()\n" + "\tfunc work(name: String) {\n" + "\t\tprint(name)\n\t}\n}\n") || + !write_source(argv[1], "Caller.swift", + "class Caller {\n" + "\tlet service = Service()\n\n" + "\t// Calls ONLY overload B, which never reaches target()\n" + "\tfunc onlyCallsOverloadB() {\n" + "\t\tself.service.work(name: \"x\")\n\t}\n}\n")) { + perror("write fixture"); + return 2; + } + printf("EVIDENCE root=%s mode=fast\n", argv[1]); + cbm_pipeline_t *p = cbm_pipeline_new(repo, db, CBM_MODE_FAST); + if (!p) { + fprintf(stderr, "ERROR pipeline_new\n"); + return 2; + } + /* Stable project name avoids differing path-derived graph identities. */ + if (!cbm_pipeline_set_project_name(p, "issue2061-swift-identity")) { + fprintf(stderr, "ERROR set_project_name\n"); + cbm_pipeline_free(p); + return 2; + } + int rc = cbm_pipeline_run(p); + if (rc != 0) { + fprintf(stderr, "ERROR pipeline_run rc=%d (not semantic RED)\n", rc); + cbm_pipeline_free(p); + return 2; + } + cbm_store_t *s = cbm_store_open_path(db); + if (!s) { + fprintf(stderr, "ERROR store_open\n"); + cbm_pipeline_free(p); + return 2; + } + const char *project = cbm_pipeline_project_name(p); + int work_count = -1; + int64_t flag = find_node(s, project, "work", "Sources/Service.swift", 5, 7, &work_count); + int64_t name = find_node(s, project, "work", "Sources/Service.swift", 10, 12, NULL); + int64_t target = find_node(s, project, "target", "Sources/Sink.swift", 2, 2, NULL); + int64_t caller = + find_node(s, project, "onlyCallsOverloadB", "Sources/Caller.swift", 5, 7, NULL); + observe("two_work_nodes", work_count, 2); + observe("flag_node_at_5_7", flag != 0, 1); + observe("name_node_at_10_12", name != 0, 1); + observe("target_node", target != 0, 1); + observe("caller_node", caller != 0, 1); + observe("flag_calls_target", calls(s, flag, target), 1); + observe("name_does_not_call_target", calls(s, name, target), 0); + observe("caller_calls_name", calls(s, caller, name), 1); + observe("caller_does_not_call_flag", calls(s, caller, flag), 0); + observe("target_inbound_depth3_has_no_false_caller", inbound(s, target, caller, 3), 0); + cbm_store_close(s); + cbm_pipeline_free(p); + printf("SUMMARY passed=%d failed=%d errors=%d exit=%d\n", passed, failed, errors, + errors ? 2 + : failed ? 1 + : 0); + return errors ? 2 : failed ? 1 : 0; +} diff --git a/tests/repro/issue2061_swift_identity/syntax.mk b/tests/repro/issue2061_swift_identity/syntax.mk new file mode 100644 index 0000000000..af2ec4522e --- /dev/null +++ b/tests/repro/issue2061_swift_identity/syntax.mk @@ -0,0 +1,5 @@ +# Invoke from an exact archived source root; no linking or test execution. +include Makefile.cbm +.PHONY: issue2061-syntax +issue2061-syntax: + $(CC) $(CFLAGS_TEST) -fsyntax-only $(ALL_TEST_SRCS) From ceb19144a9b44e789d63f0a45052b68fb2ca4ab3 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 3 Oct 2026 13:55:40 +0800 Subject: [PATCH 14/49] docs(swift): record original issue 2061 graph red-green evidence Signed-off-by: David HLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 124 ++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 8 deletions(-) diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md index 0af7a9fb9c..42285018c5 100644 --- a/docs/SWIFT_IDENTITY_VALIDATION.md +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -1,6 +1,6 @@ # Swift identity validation -This records focused local evidence for issue #2061 and PR #2436: Swift +This records local evidence for issue #2061 and PR #2436: Swift callable identity and conservative overload candidates, including candidate counts and multiple trailing closure labels. The scope remains one language, one claim. @@ -12,10 +12,13 @@ test paths committed in `fix(swift): match multiple trailing closure labels`. It was tested from a fresh `git archive` snapshot, without uncommitted infrastructure changes or Git metadata. -Public PR head S is `b0958c6cd36c68825fac55cc58934181a2251192`; it does -not contain this local A checkpoint. A's parent is local merge -`b7c86690618ed0accf137a42a987887872475fc3`. This document and the local -checkpoint code have not been pushed. PR readiness remains unestablished. +Historical publication snapshot, recorded before the checkpoint was pushed: +public PR head S was `b0958c6cd36c68825fac55cc58934181a2251192` and did +not contain the local A checkpoint. A's parent was local merge +`b7c86690618ed0accf137a42a987887872475fc3`; the checkpoint and this document +were then unpublished. That publication status is superseded: public harness +checkpoint `69e58c0df998e350d14fd9db8e8d205f7bf6036a` includes both. +PR readiness remains unestablished. | Identity | Value | | --- | --- | @@ -60,9 +63,11 @@ The repository's default ASan+UBSan settings and the ## Evidence limits and earlier failures -No attributable raw Swift negative reproduction log was found for parent -baseline `5538355530bb126c3041f7fcf8ef7a82b4bb3fec`. This evidence therefore -does not establish a red/green comparison against that parent. +At the time of the 787-pass focused run, no attributable raw Swift negative +reproduction log had been found for parent baseline +`5538355530bb126c3041f7fcf8ef7a82b4bb3fec`. That focused run alone did not +establish RED/GREEN. The subsequent original-fixture comparison below fills +the graph-layer reproduction gap without changing the focused run's scope. Earlier checks used combined working trees containing Swift and independent infrastructure changes, based on @@ -86,3 +91,106 @@ A-only archive: The focused A-only result does not establish full acceptance or PR readiness. The earlier failures remain open in their respective scopes. No broader CLI, daemon, Cypher, MCP or Store repair is included in this Swift checkpoint. + +## Original issue #2061 graph-layer RED/GREEN (2026-10-03) + +The coordinator accepted the original three-file fixture comparison as +graph-layer P RED / A GREEN. This tests the production pipeline and persisted +CALLS graph, not the `trace_path` presentation layer or all PR gates. It is +evidence for the overall issue #2061 repair, not isolated proof of the multiple +trailing-closure patch. + +| Role | Exact commit | +| --- | --- | +| P: parent baseline | `5538355530bb126c3041f7fcf8ef7a82b4bb3fec` | +| A: Swift candidate | `cf67f49dc2d718709846adefff5bab6cf9b671d4` | +| Shared external harness | `69e58c0df998e350d14fd9db8e8d205f7bf6036a` | + +Both sides used fresh full Git archives, separate source/build/repository/DB +directories, the same external `tests/repro/issue2061_swift_identity/main.c`, +and the existing production pipeline/store APIs in FAST mode. No resolver, +P/A source or harness was modified. Neither side was rebuilt or rerun. + +Linux x86_64; GCC/G++ **16.2.1 20260810**. The standard archived Makefile's +production/extraction/grammar dependencies and `CFLAGS_TEST` were retained: +C11, `-g -O1`, `-fsanitize=address,undefined`, +`-fno-omit-frame-pointer`, standard warning/feature/API defines, and +`TEST_SEAMS=1`. Path-normalized dry-run plans were identical; the only test +source was the external harness, with no full test suite compiled or run. +The build command, with evidence locations represented by variables, was: + +```bash +make -j8 -f Makefile.cbm "$BUILD/test-runner" \ + CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$BUILD" \ + ALL_TEST_SRCS="$HARNESS/main.c" +``` + +P/A inherited the same execution environment and shared fresh `TMPDIR`. +The evidence directory and compilation temporaries used disk-backed storage +after a capacity-only preflight blocked the initial tmpfs location; that +initial attempt started no archive/build/run. Environment evidence records +only the reviewed non-secret allowlist, with other CBM variables redacted. +Each harness invocation used `timeout 180s`, +`ASAN_OPTIONS=detect_leaks=1:halt_on_error=1` and +`UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1`. + +All timestamps below are UTC on **2026-10-03**: + +| Side | Build start / end | Build exit | Run start / end | Run exit | +| --- | --- | --- | --- | --- | +| P | 05:03:18.197110 / 05:19:16.170967 | 0 | 05:27:49.563068 / 05:27:49.764641 | 1 | +| A | 05:19:16.745556 / 05:27:49.016321 | 0 | 05:46:05.496977 / 05:46:05.680162 | 0 | + +P reported **4 passed, 6 failed, errors 0, exit 1**. The source's two `work` +overloads collapsed into one node at lines 10–12. The surviving name overload +had `name -> target`, the caller had `caller -> name`, and the depth-three +inbound traversal observed the false caller (`actual=1`, expected 0). +Those positive wrong-path observations establish the original defect; +the missing flag node and unavailable (`-1`) edge checks alone do not. + +A reported **10 passed, 0 failed, errors 0, exit 0**: distinct work nodes at +lines 5–7 and 10–12; flag -> target; name has no target edge; caller -> name +and no flag edge; no false caller within target's three inbound CALLS levels. +Both complete stderr files contain normal `level=info` pipeline logs, with +no sanitizer/runtime diagnostics. + +The external scheduler accepted P's expected exit 1, then incorrectly treated +any nonempty stderr as fatal, stopping on normal info logs before A ran. +An external continuation corrected only that control flow. It first checked +that no target process, A result files or A-run directory existed, verified +the already-built binaries and recorded environment, then ran A exactly once +using the original binary/options/TMPDIR. P was not rerun. The old stopping +record and original seal remain intact. + +SHA-256 fingerprints (full values): + +| Artifact | P | A | +| --- | --- | --- | +| Full Git archive | `cc83e94190a4abeff58d1add0f9e575ebc4fa7d639bb62873800e6177ba2acc6` | `c46f2286fb49e056c24cbf907ee098b798c6c5b9099343d57d80df1588e03df2` | +| Harness binary | `99d812bec8cb5ac2e7b642ff40566a363a3d9d183cce63b05c9c26af02bd0e34` | `f625323a623bc181fcaddedbe7a196d876984ec559985496ff043e37f4a16181` | +| Source manifest before/final | `ced6a6ee75e083666b3cacd1d3829ff9d2cc09225c952c826f941f33fa319fe3` | `c388f26045627f29bc7e4707331832d14c64d40d04edf15b61d06c17c9780b0a` | + +| Shared artifact | SHA-256 | +| --- | --- | +| Harness `main.c` | `d2cdee7ba8dca1965a653803c5f0d4362aa5f9f5b65c321c9aa3268b23a100c0` | +| `Caller.swift` (both sides) | `325eb2e1034f00f9c2bc02d88123e3eb06089f39f38e8e1b0ded21a6d8314aa7` | +| `Service.swift` (both sides) | `5e18290f7bbbc29c341d5ddffb51d2c19ed00b97726e2fad0d50ebcfbcd1a529` | +| `Sink.swift` (both sides) | `a847552c25bf3a5114a565c64a9f80d8fa659f45c0dff9d1a208c700724af094` | +| Main checkout dirty/untracked content manifest before/final | `6f9882e6b094e355cf8cfdaa56748fa4826e8adafd36c9eb08ca5b8a652c1ab0` | +| Final evidence SHA-256 manifest file | `ef33eceb9ed9d3cdbb0f8910159d37e4b2c5fcf87b015fabb66ec65f9011b2de` | + +All 24 continuation checks passed. Source manifests cover archived tracked +files, modes and symlink targets and match archive contents before/final. +Archive/binary hashes stayed unchanged. The fixtures match one another and +the original three embedded issue files; all harness file hashes remained +identical. Main checkout content, status, diff, index and HEAD were unchanged. +Every previously recorded evidence file was preserved byte-for-byte. +The final continuation seal is **2026-10-03T05:46:17.826586Z**. + +Complete commands, raw stdout/stderr, actual exits, UTC records, environment +allowlist and manifests remain in the retained external evidence directory; +private absolute locations and secrets are intentionally omitted here. +This comparison does not turn the historical mixed failures into passes, +establish full/TSan/lint/memory gates on the current PR candidate, or establish +PR readiness. Remaining Swift acceptance work requires a separately approved +plan and evidence tied to one exact final candidate. From 617c0305b716936be41e86b096404c8805d77566 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 3 Oct 2026 14:16:16 +0800 Subject: [PATCH 15/49] test(swift): add MCP presentation repro driver Signed-off-by: David HLP --- .../repro/issue2061_swift_identity/README.md | 139 ++++++++++++ .../issue2061_swift_identity/mcp_driver.c | 198 ++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 tests/repro/issue2061_swift_identity/mcp_driver.c diff --git a/tests/repro/issue2061_swift_identity/README.md b/tests/repro/issue2061_swift_identity/README.md index a03c5ba60e..005d173804 100644 --- a/tests/repro/issue2061_swift_identity/README.md +++ b/tests/repro/issue2061_swift_identity/README.md @@ -1,5 +1,144 @@ # Original Swift issue #2061: standalone pipeline/store proof +## Candidate MCP presentation check (not executed) + +`mcp_driver.c` is a separate, thin main using the existing MCP server API. +It is not registered in test suites or CI. Review this candidate and obtain +fresh root authorization before any backup, link or execution. No new driver +result exists for `4d99219e41c35a4dff2c1b0f211e423b913ac65f`. + +Inputs are the sealed, accepted A graph from candidate +`cf67f49dc2d718709846adefff5bab6cf9b671d4` and its original three-file fixture, +produced by harness `69e58c0df998e350d14fd9db8e8d205f7bf6036a`. +Their private locations are operator inputs, never committed artifacts. +The accepted project is `issue2061-swift-identity`. The graph DB SHA-256 is +`6e8389bdcacf62cf4956556329afc74f6312b6dbe69c3a12ffb1c0579587e4ce`; +fixture hashes are in `docs/SWIFT_IDENTITY_VALIDATION.md`. + +Layout: a new ordinary evidence directory holds `source/`, `build/`, `tmp/`, +`cache/issue2061-swift-identity.db`, logs and a fixture copy. Nothing belongs +in Git. Never reuse, overwrite, delete or modify prior evidence. Record exact +source/driver SHAs, UTC, toolchain, commands, actual exits, stdout/stderr and +before/after hashes (including original DB/sidecars and fixtures). Log only +an explicit non-secret environment allowlist; other variables are redacted. + +### Read-only consistent backup, after authorization + +The accepted DB is closed and sealed; read-only lstat/hash preflight found no +`-wal`, `-shm` or `-journal`. Reconfirm those facts and absence of writers. +Use SQLite's backup API, not a lone DB-file copy. For this sealed no-sidecar +case, `mode=ro&immutable=1` prevents SQLite from writing the original or making +sidecars. Immutable mode must NOT be used if writers or a WAL are present: +stop and obtain a separately reviewed WAL-aware snapshot plan instead. + +This template is not part of driver execution. `accepted_db`, +`accepted_fixture`, and `evidence` must be supplied privately; the destination +cache must be newly created and empty. Preserve/hash the original files and +copy the three fixture files separately without overwriting any destination. + +```python +# Run only after root approval; no database access during syntax/preflight. +import os, sqlite3 +from pathlib import Path +source = Path(os.environ["accepted_db"]).resolve(strict=True) +destination = Path(os.environ["evidence"]) / "cache/issue2061-swift-identity.db" +assert not destination.exists() +assert all(not os.path.lexists(str(source) + s) + for s in ("-wal", "-shm", "-journal")) +# Operator also checks the accepted SHA-256 and that no writer is active. +fd = os.open(destination, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) +os.close(fd) # Exclusive creation also refuses existing/broken symlinks. +uri = source.as_uri() + "?mode=ro&immutable=1" +with sqlite3.connect(uri, uri=True) as src: + with sqlite3.connect(str(destination)) as dst: + src.backup(dst) +``` + +The backup is logically identical; its physical hash may differ. Capture both +hashes. Do not rewrite project roots in it. The DB stores the original fixture +root, and production MCP workspace validation uses that root. Set +`CBM_ALLOWED_ROOT` and the server session context to the verified original +fixture root, which remains read-only. The fixture copy provides independent +byte preservation; it does not authorize changing the stored root. + +### API and assertions + +The driver calls `cbm_mcp_server_new(NULL)`, disables background tasks, selects +the analysis profile, sets explicit session/allowed roots, and submits +`tools/call` JSON-RPC through `cbm_mcp_server_handle`. Production resolve-store +opens the existing named DB in the independent `CBM_CACHE_DIR` query-only. +There is no indexing, direct store access, graph traversal or result filtering. + +Five observations are collected: + +1. `search_graph(^work$, json)`: exactly two identities at Service lines 5-7 + and 10-12, with labels/types in their QNs. The search matches the stored bare + name; grouped presentation's `name` cell is the signature-bearing QN suffix. +2. `trace_path(target, inbound, depth=3, json)`: exactly flag overload at hop1, + total1/relation eq; neither name overload nor false caller can be present. +3. The same target request with default tree output: exact singleton direct + table, total1/relation eq, full flag QN and hop1, no additional rows/cursor. + A presentation encoding change is a mismatch to inspect, not permission to + silently relax the assertion. +4. `trace_path(onlyCallsOverloadB, outbound, depth=1, json)`: only name overload + at hop1, total1/relation eq. +5. Name overload's exact QN, outbound depth1/json: zero callees, total0/eq. + +Each request prints the original request and raw response. Envelope checks +require JSON-RPC 2.0, matching ID, no error, explicit isError=false and exactly +one text item. JSON requires structuredContent to equal the parsed text object; +tree requires structuredContent absent. All JSON rows are checked, along with +columns, totals, exact relation, and absence of truncation/continuation. +Expected summary: passed=5 failed=0 errors=0 exit=0. Exit1 means a response +mismatch; exit2 means setup/envelope failure. Neither establishes a P RED. + +### Syntax/preflight now; build/run only after another review + +Use an exact unmodified production archive, not the dirty checkout Makefile. +The existing `syntax.mk` supports this driver without modification: + +```bash +make -f "$harness/syntax.mk" issue2061-syntax \ + CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$evidence/build" \ + ALL_TEST_SRCS="$harness/mcp_driver.c" +make -n -f Makefile.cbm "$evidence/build/test-runner" \ + CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$evidence/build" \ + ALL_TEST_SRCS="$harness/mcp_driver.c" +``` + +Preflight must show only the external driver as the test/main input, standard +production/grammar dependencies, no suite execution, and no flag changes. +Syntax/preflight success is not runtime acceptance. After root reviews the +pushed candidate, archive one approved full SHA into a fresh evidence directory; +retain the same GCC/G++ 16.2.1, Make 4.4.1 and standard test flags: + +```bash +export TMPDIR="$evidence/tmp" +make -j8 -f Makefile.cbm "$evidence/build/test-runner" \ + CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$evidence/build" \ + ALL_TEST_SRCS="$harness/mcp_driver.c" +export CBM_CACHE_DIR="$evidence/cache" +export CBM_ALLOWED_ROOT="$accepted_fixture" +if ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ +UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ +timeout 180s "$evidence/build/test-runner" issue2061-swift-identity \ + > "$evidence/run.stdout" 2> "$evidence/run.stderr"; then + run_exit=0 +else + run_exit=$? +fi +printf '%s\n' "$run_exit" > "$evidence/run.exit" +# Preserve complete stderr; an info log is not a runner failure. +``` + +Stop on input/hash/SHA mismatch, unexpected sidecars/writers, nonempty existing +destination, expanded dry-run, compiler failure, timeout/signal, sanitizer +diagnostic, invalid/error response, missing rows or incomplete output. Normal +info stderr is not fatal. Preserve the checkpoint/evidence and report; do not +change production code, rerun P, or repair global baselines. This stage cannot +establish incremental multi-trailing-closure, full-suite, CI, DCO or maintainer +acceptance. + This runner embeds only the three original Swift files from [issue #2061](https://github.com/DeusData/codebase-memory-mcp/issues/2061). It uses the real production pipeline in FAST mode and queries its persisted diff --git a/tests/repro/issue2061_swift_identity/mcp_driver.c b/tests/repro/issue2061_swift_identity/mcp_driver.c new file mode 100644 index 0000000000..8862f2fc4c --- /dev/null +++ b/tests/repro/issue2061_swift_identity/mcp_driver.c @@ -0,0 +1,198 @@ +/* Read the reviewed A graph COPY through production MCP only. No indexing, + * store queries, graph traversal, resolver replacement, or fixture creation. + * Exit 0: all response checks pass; 1: mismatch; 2: setup/envelope error. + */ +#include "mcp/mcp.h" +#include + +#include +#include +#include + +static int passed, failed, errors; + +static void check(const char *name, bool ok) { + printf("OBS %s %s\n", name, ok ? "PASS" : "FAIL"); + if (ok) + passed++; + else + failed++; +} + +static bool number(yyjson_val *obj, const char *key, int expected) { + yyjson_val *v = yyjson_obj_get(obj, key); + return yyjson_is_int(v) && yyjson_get_int(v) == expected; +} + +static bool complete(yyjson_val *obj) { + const char *flags[] = {"truncated", "has_more", "engine_saturated", + "output_budget_floor_exceeded"}; + const char *cursors[] = {"next", "next_cursor", "next_offset", "cursor"}; + for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { + yyjson_val *v = yyjson_obj_get(obj, flags[i]); + if (v && !yyjson_is_false(v)) + return false; + } + for (size_t i = 0; i < sizeof(cursors) / sizeof(cursors[0]); i++) { + if (yyjson_obj_get(obj, cursors[i])) + return false; + } + return true; +} + +/* Decode returned presentation rows, never graph data. Every row must match; + * no filtering can turn an unwanted row into a successful absence check. */ +static bool rows(yyjson_val *obj, const char *project, bool search, const char *suffix, + int expected) { + yyjson_val *cols = yyjson_obj_get(obj, "cols"); + yyjson_val *groups = yyjson_obj_get(obj, "groups"); + if (!yyjson_is_arr(cols) || !yyjson_is_arr(groups) || + !yyjson_equals_str(yyjson_arr_get(cols, 0), "name") || + !yyjson_equals_str(yyjson_arr_get(cols, 1), search ? "label" : "hop") || + yyjson_arr_size(cols) != (search ? 5U : 2U)) + return false; + int count = 0, flag = 0, name = 0; + size_t gi, gm, ri, rm; + yyjson_val *group, *row; + yyjson_arr_foreach(groups, gi, gm, group) { + const char *prefix = yyjson_get_str(yyjson_obj_get(group, "qn_prefix")); + yyjson_val *values = yyjson_obj_get(group, "rows"); + if (!prefix || !yyjson_is_arr(values)) + return false; + yyjson_arr_foreach(values, ri, rm, row) { + const char *leaf = yyjson_get_str(yyjson_arr_get(row, 0)); + char qn[2048], wanted[2048]; + if (!leaf || !yyjson_is_arr(row) || yyjson_arr_size(row) != yyjson_arr_size(cols) || + snprintf(qn, sizeof(qn), "%s%s%s", prefix, *prefix ? "." : "", leaf) >= + (int)sizeof(qn)) + return false; + count++; + if (search) { + snprintf(wanted, sizeof(wanted), "%s.Sources.Service.Service.work(flag:Bool)", + project); + if (strcmp(qn, wanted) == 0 && + yyjson_equals_str(yyjson_arr_get(row, 2), "5-7")) + flag++; + else { + snprintf(wanted, sizeof(wanted), "%s.Sources.Service.Service.work(name:String)", + project); + if (strcmp(qn, wanted) != 0 || + !yyjson_equals_str(yyjson_arr_get(row, 2), "10-12")) + return false; + name++; + } + if (!yyjson_equals_str(yyjson_obj_get(group, "file"), "Sources/Service.swift") || + !yyjson_equals_str(yyjson_arr_get(row, 1), "Method")) + return false; + } else { + snprintf(wanted, sizeof(wanted), "%s.%s", project, suffix); + yyjson_val *hop = yyjson_arr_get(row, 1); + if (strcmp(qn, wanted) != 0 || !yyjson_is_int(hop) || yyjson_get_int(hop) != 1) + return false; + } + } + } + return count == expected && (!search || (flag == 1 && name == 1)); +} + +static void request(cbm_mcp_server_t *srv, const char *project, int id, const char *tool, + const char *args, bool search, bool tree, const char *suffix, int expected) { + char input[4096], label[64]; + snprintf(label, sizeof(label), "request_%d", id); + int n = snprintf(input, sizeof(input), + "{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"%s\",\"arguments\":%s}}", id, tool, args); + if (n < 0 || n >= (int)sizeof(input)) { + errors++; + return; + } + printf("REQUEST %d %s\n", id, input); + char *raw = cbm_mcp_server_handle(srv, input); + printf("RAW %d %s\n", id, raw ? raw : "null"); + yyjson_doc *doc = raw ? yyjson_read(raw, strlen(raw), 0) : NULL; + yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL; + yyjson_val *result = yyjson_obj_get(root, "result"); + yyjson_val *content = yyjson_obj_get(result, "content"); + yyjson_val *item = yyjson_arr_get(content, 0); + const char *text = yyjson_get_str(yyjson_obj_get(item, "text")); + yyjson_val *structured = yyjson_obj_get(result, "structuredContent"); + bool envelope = yyjson_equals_str(yyjson_obj_get(root, "jsonrpc"), "2.0") && + number(root, "id", id) && !yyjson_obj_get(root, "error") && + yyjson_is_obj(result) && yyjson_is_false(yyjson_obj_get(result, "isError")) && + yyjson_is_arr(content) && yyjson_arr_size(content) == 1 && + yyjson_equals_str(yyjson_obj_get(item, "type"), "text") && text; + if (!envelope) { + fprintf(stderr, "ERROR request_%d invalid/error envelope\n", id); + errors++; + } else if (tree) { + /* This singleton fixture selects the smaller direct table. Exact text + * checks total, relation, one identity/hop, and absence of continuation + * or extra rows; an encoding change is a reviewable mismatch. */ + char wanted[2048]; + snprintf(wanted, sizeof(wanted), + "function: target\ndirection: inbound\ncallers_total: 1\n" + "callers_total_relation: eq\ncallers: 1 (cols: qn hop)\n" + " %s.Sources.Service.Service.work(flag:Bool) 1\n", project); + check(label, !structured && strcmp(text, wanted) == 0); + } else { + yyjson_doc *payload = yyjson_read(text, strlen(text), 0); + yyjson_val *body = payload ? yyjson_doc_get_root(payload) : NULL; + bool ok = yyjson_is_obj(structured) && yyjson_is_obj(body) && + yyjson_equals(structured, body) && complete(body); + if (search) { + ok = ok && number(body, "total", 2) && number(body, "returned", 2) && + rows(body, project, true, NULL, 2); + } else { + const char *leg = id == 2 ? "callers" : "callees"; + const char *total = id == 2 ? "callers_total" : "callees_total"; + const char *relation = id == 2 ? "callers_total_relation" : "callees_total_relation"; + ok = ok && number(body, total, expected) && + yyjson_equals_str(yyjson_obj_get(body, relation), "eq") && + rows(yyjson_obj_get(body, leg), project, false, suffix, expected); + } + check(label, ok); + yyjson_doc_free(payload); + } + yyjson_doc_free(doc); + free(raw); +} + +int main(int argc, char **argv) { + if (argc != 2 || strcmp(argv[1], "issue2061-swift-identity") != 0 || + !getenv("CBM_CACHE_DIR") || !getenv("CBM_ALLOWED_ROOT")) { + fprintf(stderr, "usage: mcp-driver issue2061-swift-identity; set isolated " + "CBM_CACHE_DIR and CBM_ALLOWED_ROOT\n"); + return 2; + } + const char *project = argv[1]; + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + if (!srv) + return 2; + cbm_mcp_server_set_background_tasks(srv, false); + cbm_mcp_server_set_tool_profile(srv, CBM_MCP_TOOL_PROFILE_ANALYSIS); + if (!cbm_mcp_server_set_session_context(srv, getenv("CBM_ALLOWED_ROOT"), + getenv("CBM_ALLOWED_ROOT"))) { + cbm_mcp_server_free(srv); + return 2; + } + char args[2048]; + snprintf(args, sizeof(args), "{\"project\":\"%s\",\"name_pattern\":\"^work$\"," + "\"format\":\"json\",\"limit\":20}", project); + request(srv, project, 1, "search_graph", args, true, false, NULL, 2); + for (int id = 2; id <= 5; id++) { + const char *function = id <= 3 ? "target" : id == 4 ? "onlyCallsOverloadB" : + "issue2061-swift-identity.Sources.Service.Service.work(name:String)"; + snprintf(args, sizeof(args), "{\"project\":\"%s\",\"function_name\":\"%s\"," + "\"direction\":\"%s\",\"depth\":%d,\"limit\":100," + "\"max_output_tokens\":3200%s}", project, function, + id <= 3 ? "inbound" : "outbound", id <= 3 ? 3 : 1, + id == 3 ? "" : ",\"format\":\"json\""); + request(srv, project, id, "trace_path", args, false, id == 3, + id == 2 ? "Sources.Service.Service.work(flag:Bool)" : + "Sources.Service.Service.work(name:String)", id == 5 ? 0 : 1); + } + cbm_mcp_server_free(srv); + int rc = errors ? 2 : failed ? 1 : 0; + printf("SUMMARY passed=%d failed=%d errors=%d exit=%d\n", passed, failed, errors, rc); + return rc; +} From f6c5ef6f15b94aada5602240f86d1c9b692455fb Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 3 Oct 2026 14:56:12 +0800 Subject: [PATCH 16/49] docs(swift): record scoped MCP validation failure Signed-off-by: David HLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 22 +++++++++++++++++++ .../repro/issue2061_swift_identity/README.md | 10 ++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md index 42285018c5..ce2af2d6f3 100644 --- a/docs/SWIFT_IDENTITY_VALIDATION.md +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -194,3 +194,25 @@ This comparison does not turn the historical mixed failures into passes, establish full/TSan/lint/memory gates on the current PR candidate, or establish PR readiness. Remaining Swift acceptance work requires a separately approved plan and evidence tied to one exact final candidate. + +## MCP presentation attempt — FAIL, 2026-10-03 + +Tested candidate: `0007c1d22858e1548ca392f382275595a0a2c691`; one exact-archive build/run, no reindexing. +Build: **06:26:37.439977–06:35:25.810701 UTC**, exit **0**; run: **06:35:26.198188–06:35:26.380432 UTC**, exit **1**. +Standard GCC/G++ 16.2.1 ASan+UBSan/test-seams flags and leak detection remained enabled. + +Requests 1–4 had OBS PASS: two work identities; target inbound depth3 JSON and default tree (only flag overload, hop1, total1/eq); caller outbound (only name overload, hop1, total1/eq). +All five MCP status logs were `ok`, but stdout ends at `RAW 5 {`; request 5's complete response, OBS and final SUMMARY were not flushed and its semantics remain unverified. +LeakSanitizer reported a **1024-byte direct leak**; the entire run failed, not five observations passed or a semantic RED. +Key stack: [find_nodes_generic / Store](https://github.com/DavidHLP/codebase-memory-mcp/blob/0007c1d22858e1548ca392f382275595a0a2c691/src/store/store.c#L2779) → `cbm_store_find_nodes_by_name` → [handle_trace_call_path / MCP](https://github.com/DavidHLP/codebase-memory-mcp/blob/0007c1d22858e1548ca392f382275595a0a2c691/src/mcp/mcp.c#L9291). +The complete MCP and Store source files are byte-identical to parent P `5538355530bb126c3041f7fcf8ef7a82b4bb3fec`, as confirmed by source comparison; this does not claim a new P runtime result. +This inherited generic leak is **OUT OF SCOPE**: it is not included in the Swift fix or an additional Swift acceptance gate. No repair, suppression or rerun was performed. +Original DB, backup copy, fixtures, archived source, binary and main checkout were **unchanged**; independent cache and DB copy provided isolation, not a query-layer stored-root check. +Original graph-layer P RED/A GREEN remains unchanged; the **787 focused passes belong only to core `761ea8ca432ae0f58a6bb77c438ccedcb1ade3a1`**. + +Original seal: **2026-10-03T06:35:33.549096Z**; evidence-manifest SHA-256: `0ea8f6f2de4cc10cc9b93234f557ed2aca774abcdf859982aa972691cc4bfadb`. +Raw evidence and the verbatim 69-line record are retained externally; that record's SHA-256 is `cb6278aa60003baeb4e0ccbf47ca39f72af546d389d29fa708e428b2eb7402de`. Existing evidence and seal were not overwritten. + +The [owner's issue decision](https://github.com/DeusData/codebase-memory-mcp/issues/2061#issuecomment-5836287644) requests Swift on parent PR2342, centered on helper/regression coverage: stable labels+types QNs with bare names, compatible label/default/trailing-closure selection, all compatible candidate edges with counts, and an index-format rebuild. +The [owner's PR reply](https://github.com/DeusData/codebase-memory-mcp/pull/2436#issuecomment-5936875903) queues deeper review; it is neither approval nor a routine-rebase request. +The [automated PR acknowledgement](https://github.com/DeusData/codebase-memory-mcp/pull/2436#issuecomment-5902448775) asks for CI green **or an explanation of believed pre-existing failures**; full/native macOS/strict-P matrices were contributor validation designs or commitments, not owner-specified Swift requirements. diff --git a/tests/repro/issue2061_swift_identity/README.md b/tests/repro/issue2061_swift_identity/README.md index 005d173804..d01d9ab838 100644 --- a/tests/repro/issue2061_swift_identity/README.md +++ b/tests/repro/issue2061_swift_identity/README.md @@ -55,11 +55,11 @@ with sqlite3.connect(uri, uri=True) as src: ``` The backup is logically identical; its physical hash may differ. Capture both -hashes. Do not rewrite project roots in it. The DB stores the original fixture -root, and production MCP workspace validation uses that root. Set -`CBM_ALLOWED_ROOT` and the server session context to the verified original -fixture root, which remains read-only. The fixture copy provides independent -byte preservation; it does not authorize changing the stored root. +hashes. Do not rewrite project roots in it. Isolation comes from the independent +`CBM_CACHE_DIR` and DB copy: embedded MCP `search_graph`/`trace_path` do not +validate the stored root against `CBM_ALLOWED_ROOT`. The server session context +may retain the verified original fixture root, which remains read-only; it is +not a query-layer isolation check. The fixture copy preserves the source bytes. ### API and assertions From eea3143982260cd072db0b76287ff4b85309e304 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sat, 3 Oct 2026 16:55:08 +0800 Subject: [PATCH 17/49] docs(swift): record index format rebuild validation Signed-off-by: David HLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 52 +++++++++++++++++++ .../repro/issue2061_swift_identity/README.md | 15 ++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md index ce2af2d6f3..98be82a1af 100644 --- a/docs/SWIFT_IDENTITY_VALIDATION.md +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -216,3 +216,55 @@ Raw evidence and the verbatim 69-line record are retained externally; that recor The [owner's issue decision](https://github.com/DeusData/codebase-memory-mcp/issues/2061#issuecomment-5836287644) requests Swift on parent PR2342, centered on helper/regression coverage: stable labels+types QNs with bare names, compatible label/default/trailing-closure selection, all compatible candidate edges with counts, and an index-format rebuild. The [owner's PR reply](https://github.com/DeusData/codebase-memory-mcp/pull/2436#issuecomment-5936875903) queues deeper review; it is neither approval nor a routine-rebase request. The [automated PR acknowledgement](https://github.com/DeusData/codebase-memory-mcp/pull/2436#issuecomment-5902448775) asks for CI green **or an explanation of believed pre-existing failures**; full/native macOS/strict-P matrices were contributor validation designs or commitments, not owner-specified Swift requirements. + +## Existing index_format suite — 2026-10-03 + +One authorized invocation of the standard test entry point passed **3/3**, +exit **0**, **08:48:58.454697–08:48:59.120654 UTC**. `make -q` returned **0** +and the entry-point build reported the existing runner up to date: no rebuild. +The archived source/core was `761ea8ca432ae0f58a6bb77c438ccedcb1ade3a1`; +the then-public candidate was `34a370ce2debd9ce5721ad99d8c7be67ef3ffa2b`. +All core-to-candidate changes were validation docs or standalone repro files, +not runner inputs. Makefile, test entry point and test source matched candidate +HEAD. Working-tree WIP was excluded. GCC/G++ **16.2.1 20260810**, Make **4.4.1**, +default ASan+UBSan, and `sanitized=1 test_seams=1` build metadata were retained. + +Exact invocation, with private path bindings recorded in external evidence: + +```bash +cd "$EXACT_SOURCE" +TMPDIR="$EVIDENCE/tmp" scripts/test.sh --suites index_format \ + BUILD_DIR="$EXISTING_BUILD" CC=gcc CXX=g++ TEST_SEAMS=1 +``` + +`index_format_siblings_distinct_and_searchable`, +`index_format_legacy_index_rebuilds_and_repairs`, and +`index_format_version_one_rebuilds` passed. The last test writes format **1**, +asserts full-rebuild routing and `format_migration:true`, verifies current +format **2** and preserved file count, then checks no second rebuild on an +unchanged run. This is narrow rebuild-boundary evidence, not a full-suite or +native-macOS acceptance result; the earlier 787 result keeps its original SHA. + +Before/after archive, source manifest, runner, test source, entire old build +and main checkout were unchanged. Complete stdout/stderr and real exit/UTC +records are preserved in a new independent `/home` evidence directory; hardcoded +test fixtures still used `/tmp`, whose available space was checked separately. +No sanitizer diagnostic occurred and no other suite or lint was run. + +| SHA-256 artifact | Value | +| --- | --- | +| Archive, before/after | `d934685cc5e521060a8ddb29dcb65e5961c10d103763375e647a9e9da6a1617e` | +| Source content manifest, before/after | `10011442026e4c5a4900c6b24ee19dda1ea3c1eda15e8bf88b0eec35b4ef77de` | +| Existing runner, before/after | `ad35773a1bc097cf980e67a4215baaee1b26337560696db85a908ce3ec2df11c` | +| tests/test_index_format.c, archive and checkout | `8599852fefeee42d879b38f22305cb4e3e309ead1b16bd7e6d8954a8358f43f4` | +| New suite evidence manifest | `2ebd57e301f1e5ea3049fc5e9ed5014c0991222d57662642d6f9d778c93135a7` | +| Later build-config metadata supplement manifest | `3ab556d8a8cbf66cf45c733624a6a460ed6b7de55c3ced539bcf4e6eebdf172c` | + +Allocator scope decision: retain the existing paired +`cbm_alloc(CBM_MEM_CLASS_OTHER, ...)` / `cbm_free(CBM_MEM_CLASS_OTHER, ...)` +suffix-scan scratch hunk in `cbm_registry_find_ending_with`, introduced by +`bbd0957937ee01c483c04fe5d20f23f72bcd1fc5`. It is not Swift semantics, but +reverting those two calls increases `src/pipeline/registry.c` raw sites from +the checked-in ratchet **17** to **19**, failing lint-memory-core/lint-ci. +The allocator pair and baseline counts were not changed; this scope rationale +does not claim a fresh lint run or authorize other allocator work. diff --git a/tests/repro/issue2061_swift_identity/README.md b/tests/repro/issue2061_swift_identity/README.md index d01d9ab838..2a38fa77a2 100644 --- a/tests/repro/issue2061_swift_identity/README.md +++ b/tests/repro/issue2061_swift_identity/README.md @@ -1,11 +1,18 @@ # Original Swift issue #2061: standalone pipeline/store proof -## Candidate MCP presentation check (not executed) +## Candidate MCP presentation check (executed; FAILED) `mcp_driver.c` is a separate, thin main using the existing MCP server API. -It is not registered in test suites or CI. Review this candidate and obtain -fresh root authorization before any backup, link or execution. No new driver -result exists for `4d99219e41c35a4dff2c1b0f211e423b913ac65f`. +It is not registered in test suites or CI. Candidate +`0007c1d22858e1548ca392f382275595a0a2c691` was run once: build exit 0, +run exit 1 with a 1024-byte LeakSanitizer leak. Observations 1–4 passed; +observation 5's response/OBS and final SUMMARY were not fully saved and remain +unverified. The separate `4d99219e41c35a4dff2c1b0f211e423b913ac65f` +checkpoint was not the executed candidate. See the +[published failure record](https://github.com/DavidHLP/codebase-memory-mcp/blob/34a370ce2debd9ce5721ad99d8c7be67ef3ffa2b/docs/SWIFT_IDENTITY_VALIDATION.md#mcp-presentation-attempt--fail-2026-10-03). +The inherited generic MCP/Store leak is **OUT OF SCOPE**; original graph-layer +P RED/A GREEN is unchanged. Obtain fresh root authorization before any further +backup, build or execution; the five-observation attempt did not pass. Inputs are the sealed, accepted A graph from candidate `cf67f49dc2d718709846adefff5bab6cf9b671d4` and its original three-file fixture, From b4e3daf0fdfe04828ba0d4070b9d1d2b72eaf5ad Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 09:44:41 +0800 Subject: [PATCH 18/49] test(swift): reproduce default-before-required argument matching Signed-off-by: DavidHLP --- tests/test_registry.c | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/test_registry.c b/tests/test_registry.c index 8f331e4aea..2097eedc09 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1311,16 +1311,19 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { "proj.Service.batch(completions:(()=>Void)~)", "proj.Service.sheet(onDismiss:(()=>Void)?,content:()=>Void)", "proj.Service.combine(callbacks:(()=>Void)~,completion:()=>Void)", + "proj.Service.repeated(x:Int,x:Int)", + "proj.Service.repeated(x:String,x:String)", + "proj.Service.unlabeled(_:Int,_:Int)", }; const char *names[] = {"work", "work", "work", "work", "send", "configure", "consume", "consume", "accept", "perform", "perform", "perform", "perform", "collect", "named", "run", "batch", - "sheet", "combine"}; - const uint8_t counts[] = {1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2}; - for (int i = 0; i < 19; i++) { + "sheet", "combine", "repeated", "repeated", "unlabeled"}; + const uint8_t counts[] = {1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 2}; + for (int i = 0; i < 22; i++) { cbm_registry_add(r, names[i], qns[i], "Method"); uint64_t defaults = (i == 5 || i == 15) ? UINT64_C(1) << 1 - : i == 17 ? UINT64_C(1) + : i == 17 || i >= 19 ? UINT64_C(1) : 0; cbm_registry_set_swift_signature(r, qns[i], defaults, counts[i]); } @@ -1358,6 +1361,25 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); ASSERT_STR_EQ(out[0], qns[5]); + /* The first default must remain skippable even when its label matches + * the argument needed by the following required parameter. */ + call.callee_name = "Service.repeated"; + call.arg_count = 1; + args[0].keyword = "x"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 2); + call.arg_count = 2; + args[1].keyword = "x"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 2); + call.arg_count = 0; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.arg_count = 1; + args[0].keyword = "wrong"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.callee_name = "Service.unlabeled"; + args[0].keyword = NULL; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[21]); + args[0].keyword = "value"; call.callee_name = "send"; call.arg_count = 1; From 233f5693a4302065ab8a680ce5c507aed95cec96 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 09:51:00 +0800 Subject: [PATCH 19/49] fix(swift): propagate route mounts through overload emitters Signed-off-by: DavidHLP --- src/pipeline/pass_calls.c | 2 +- src/pipeline/pass_parallel.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index b37ce15ae8..0ec96679d0 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -763,7 +763,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM .confidence = swift_candidates == 1 ? 0.90 : 0.55, .candidate_count = swift_candidates}; emit_classified_edge(ctx, call, source_node, target, &selected, module_qn, - imp_keys, imp_vals, imp_count, false); + imp_keys, imp_vals, imp_count, false, route_mount); emitted++; } } diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 4a42770659..9331e3c9d9 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -3239,7 +3239,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB .candidate_count = count}; emit_service_edge(ws->local_edge_buf, source_node, target, call, &selected, module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals, - imp_count, false); + imp_count, false, route_mount); ws->calls_resolved++; } continue; From 9aad2f992afda0c1bb34eeec1ec5739df9fc8a31 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 09:52:13 +0800 Subject: [PATCH 20/49] fix(swift): retain default argument matching alternatives Signed-off-by: DavidHLP --- internal/cbm/service_patterns.c | 1 - src/pipeline/registry.c | 66 +++-- .../issue2061_swift_identity/mcp_driver.c | 39 +-- tests/test_extraction.c | 19 +- tests/test_pipeline.c | 225 ++++++++++++------ tests/test_registry.c | 17 +- 6 files changed, 248 insertions(+), 119 deletions(-) diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 9028927daf..97e36554bd 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -13,7 +13,6 @@ #include "service_patterns.h" #include "callable_sig.h" - #include #include #include diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 0dfdb85bd2..8b3fba16f0 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1690,6 +1690,19 @@ static int swift_variadic_advance(const CBMCall *call, int arg, bool label_match return arg; } +static uint16_t swift_trailing_advance(const swift_parameters_t *params, + const swift_signature_t *meta, const CBMCall *call, + unsigned i, unsigned total, uint16_t states) { + uint16_t next = 0; + for (unsigned used = 0; used < total; used++) { + if ((states & (UINT16_C(1) << used)) && + swift_trailing_binds(meta, call, params, i, total, used)) { + next |= (uint16_t)(UINT16_C(1) << (used + SKIP_ONE)); + } + } + return next; +} + static bool swift_parameters_match(const swift_parameters_t *params, const swift_signature_t *meta, const CBMCall *call) { /* Preserve the legacy bool as one unlabelled closure for manual callers. */ @@ -1697,34 +1710,47 @@ static bool swift_parameters_match(const swift_parameters_t *params, const swift if (trailing_total == 0 && call->swift_trailing_closure) { trailing_total = SKIP_ONE; } - unsigned trailing_used = 0; - int arg = 0; + /* Each argument cursor retains the reachable trailing-closure counts. + * A matching label must not greedily consume an earlier default when + * the same label is needed by a later required parameter. */ + uint16_t states[CBM_MAX_CALL_ARGS + SKIP_ONE] = {1}; for (unsigned i = 0; i < params->count; i++) { - bool label_matches = swift_argument_label_matches(params, call, i, arg); - bool takes_trailing = - swift_trailing_binds(meta, call, params, i, trailing_total, trailing_used); - if (params->variadics[i]) { - arg = swift_variadic_advance(call, arg, label_matches); - if (takes_trailing && arg == call->arg_count) { - trailing_used++; + uint16_t next[CBM_MAX_CALL_ARGS + SKIP_ONE] = {0}; + for (int arg = 0; arg <= call->arg_count; arg++) { + if (!states[arg]) { + continue; } - continue; - } - if (label_matches) { - arg++; - } else if (takes_trailing && arg == call->arg_count) { - trailing_used++; - } else if ((meta->defaults & (UINT64_C(1) << i)) == 0) { - return false; + uint16_t trailing = + arg == call->arg_count + ? swift_trailing_advance(params, meta, call, i, trailing_total, states[arg]) + : 0; + if (params->variadics[i] || (meta->defaults & (UINT64_C(1) << i))) { + /* After explicit arguments, preserve the first eligible + * trailing binding instead of skipping it to a later one. */ + next[arg] |= states[arg] & (uint16_t)~(trailing >> SKIP_ONE); + } + if (swift_argument_label_matches(params, call, i, arg)) { + int end = + params->variadics[i] ? swift_variadic_advance(call, arg, true) : arg + SKIP_ONE; + next[end] |= states[arg]; + if (params->variadics[i] && end == call->arg_count) { + next[end] |= swift_trailing_advance(params, meta, call, i, trailing_total, + states[arg]); + } + } + next[arg] |= trailing; } + memcpy(states, next, sizeof(states)); } - return arg == call->arg_count && trailing_used == trailing_total; + return (states[call->arg_count] & (UINT16_C(1) << trailing_total)) != 0; } static bool swift_signature_matches(const char *qn, const char *name, const swift_signature_t *meta, const CBMCall *call) { - if (!meta || meta->count == UINT8_MAX || call->swift_args_truncated || - call->swift_trailing_truncated || call->swift_trailing_count > CBM_MAX_TRAILING_CLOSURES) { + if (!meta || meta->count == UINT8_MAX || call->arg_count < 0 || + call->arg_count > CBM_MAX_CALL_ARGS || (call->arg_count > 0 && !call->args) || + call->swift_args_truncated || call->swift_trailing_truncated || + call->swift_trailing_count > CBM_MAX_TRAILING_CLOSURES) { return false; } swift_parameters_t params; diff --git a/tests/repro/issue2061_swift_identity/mcp_driver.c b/tests/repro/issue2061_swift_identity/mcp_driver.c index 8862f2fc4c..2980b3221b 100644 --- a/tests/repro/issue2061_swift_identity/mcp_driver.c +++ b/tests/repro/issue2061_swift_identity/mcp_driver.c @@ -70,8 +70,7 @@ static bool rows(yyjson_val *obj, const char *project, bool search, const char * if (search) { snprintf(wanted, sizeof(wanted), "%s.Sources.Service.Service.work(flag:Bool)", project); - if (strcmp(qn, wanted) == 0 && - yyjson_equals_str(yyjson_arr_get(row, 2), "5-7")) + if (strcmp(qn, wanted) == 0 && yyjson_equals_str(yyjson_arr_get(row, 2), "5-7")) flag++; else { snprintf(wanted, sizeof(wanted), "%s.Sources.Service.Service.work(name:String)", @@ -101,7 +100,8 @@ static void request(cbm_mcp_server_t *srv, const char *project, int id, const ch snprintf(label, sizeof(label), "request_%d", id); int n = snprintf(input, sizeof(input), "{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"tools/call\"," - "\"params\":{\"name\":\"%s\",\"arguments\":%s}}", id, tool, args); + "\"params\":{\"name\":\"%s\",\"arguments\":%s}}", + id, tool, args); if (n < 0 || n >= (int)sizeof(input)) { errors++; return; @@ -132,7 +132,8 @@ static void request(cbm_mcp_server_t *srv, const char *project, int id, const ch snprintf(wanted, sizeof(wanted), "function: target\ndirection: inbound\ncallers_total: 1\n" "callers_total_relation: eq\ncallers: 1 (cols: qn hop)\n" - " %s.Sources.Service.Service.work(flag:Bool) 1\n", project); + " %s.Sources.Service.Service.work(flag:Bool) 1\n", + project); check(label, !structured && strcmp(text, wanted) == 0); } else { yyjson_doc *payload = yyjson_read(text, strlen(text), 0); @@ -158,8 +159,8 @@ static void request(cbm_mcp_server_t *srv, const char *project, int id, const ch } int main(int argc, char **argv) { - if (argc != 2 || strcmp(argv[1], "issue2061-swift-identity") != 0 || - !getenv("CBM_CACHE_DIR") || !getenv("CBM_ALLOWED_ROOT")) { + if (argc != 2 || strcmp(argv[1], "issue2061-swift-identity") != 0 || !getenv("CBM_CACHE_DIR") || + !getenv("CBM_ALLOWED_ROOT")) { fprintf(stderr, "usage: mcp-driver issue2061-swift-identity; set isolated " "CBM_CACHE_DIR and CBM_ALLOWED_ROOT\n"); return 2; @@ -171,25 +172,31 @@ int main(int argc, char **argv) { cbm_mcp_server_set_background_tasks(srv, false); cbm_mcp_server_set_tool_profile(srv, CBM_MCP_TOOL_PROFILE_ANALYSIS); if (!cbm_mcp_server_set_session_context(srv, getenv("CBM_ALLOWED_ROOT"), - getenv("CBM_ALLOWED_ROOT"))) { + getenv("CBM_ALLOWED_ROOT"))) { cbm_mcp_server_free(srv); return 2; } char args[2048]; - snprintf(args, sizeof(args), "{\"project\":\"%s\",\"name_pattern\":\"^work$\"," - "\"format\":\"json\",\"limit\":20}", project); + snprintf(args, sizeof(args), + "{\"project\":\"%s\",\"name_pattern\":\"^work$\"," + "\"format\":\"json\",\"limit\":20}", + project); request(srv, project, 1, "search_graph", args, true, false, NULL, 2); for (int id = 2; id <= 5; id++) { - const char *function = id <= 3 ? "target" : id == 4 ? "onlyCallsOverloadB" : - "issue2061-swift-identity.Sources.Service.Service.work(name:String)"; - snprintf(args, sizeof(args), "{\"project\":\"%s\",\"function_name\":\"%s\"," + const char *function = + id <= 3 ? "target" + : id == 4 ? "onlyCallsOverloadB" + : "issue2061-swift-identity.Sources.Service.Service.work(name:String)"; + snprintf(args, sizeof(args), + "{\"project\":\"%s\",\"function_name\":\"%s\"," "\"direction\":\"%s\",\"depth\":%d,\"limit\":100," - "\"max_output_tokens\":3200%s}", project, function, - id <= 3 ? "inbound" : "outbound", id <= 3 ? 3 : 1, + "\"max_output_tokens\":3200%s}", + project, function, id <= 3 ? "inbound" : "outbound", id <= 3 ? 3 : 1, id == 3 ? "" : ",\"format\":\"json\""); request(srv, project, id, "trace_path", args, false, id == 3, - id == 2 ? "Sources.Service.Service.work(flag:Bool)" : - "Sources.Service.Service.work(name:String)", id == 5 ? 0 : 1); + id == 2 ? "Sources.Service.Service.work(flag:Bool)" + : "Sources.Service.Service.work(name:String)", + id == 5 ? 0 : 1); } cbm_mcp_server_free(srv); int rc = errors ? 2 : failed ? 1 : 0; diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 9e9c6f8805..9e693f7774 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -2115,11 +2115,11 @@ TEST(swift_method_call) { } TEST(swift_multiple_trailing_closure_extraction) { - CBMFileResult *r = extract( - "func outer() { service.handle { nested() } onError: { recover() } }\n" - "func one() { service.run { nested() } }\n" - "func incomplete() { service.handle { } onError: { recover( } }\n", - CBM_LANG_SWIFT, "t", "Calls.swift"); + CBMFileResult *r = + extract("func outer() { service.handle { nested() } onError: { recover() } }\n" + "func one() { service.run { nested() } }\n" + "func incomplete() { service.handle { } onError: { recover( } }\n", + CBM_LANG_SWIFT, "t", "Calls.swift"); ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); CBMCall *multi = NULL; @@ -5050,10 +5050,11 @@ TEST(swift_non_url_constructor_untouched_issue1892) { * the per-file constant map and resolved at the call site, for both return * statements and arrow expression bodies. */ TEST(extract_ts_await_generic_call_issue2210) { - CBMFileResult *r = extract("function parseJsonBody() { return {} as T; }\n" - "async function plain() { return await parseJsonBody(); }\n" - "async function generic() { return await parseJsonBody(); }\n", - CBM_LANG_TYPESCRIPT, "t", "await.ts"); + CBMFileResult *r = + extract("function parseJsonBody() { return {} as T; }\n" + "async function plain() { return await parseJsonBody(); }\n" + "async function generic() { return await parseJsonBody(); }\n", + CBM_LANG_TYPESCRIPT, "t", "await.ts"); ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); ASSERT_EQ(count_calls_named(r, "parseJsonBody"), 2); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index b3f25d50e3..1a1dcbcc75 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8293,50 +8293,54 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { "class Sink {\n" " func target() {}\n" "}\n"); - write_temp_file(tmp, "Sources/Service.swift", - "class Service {\n" - " let sink = Sink()\n" - "\n" - " // Overload A: DOES call target()\n" - " func work(flag: Bool) {\n" - " self.sink.target()\n" - " }\n" - "\n" - " // Overload B: does NOT call target()\n" - " func work(name: String) {\n" - " print(name)\n" - " }\n" - " func upload(_ data: Data, to url: URL) { sink.target() }\n" - " func upload(_ fileURL: URL, to url: URL) { print(fileURL) }\n" - " func record(path: String, session: URLSession) {}\n" - " func withDefault(message: String, flag: Bool = true) { sink.target() }\n" - " func withCallback(completion: () -> Void) { sink.target() }\n" - " func many(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, h: Int) {}\n" - " func handle(completion: () -> Void) { sink.target() }\n" - " func handle(completion: () -> Void, onError: () -> Void) { sink.target() }\n" - "}\n"); - write_temp_file(tmp, "Sources/Caller.swift", - "class Caller {\n" - " let service = Service()\n" - "\n" - " // Calls ONLY overload B, which never reaches target()\n" - " func onlyCallsOverloadB() {\n" - " self.service.work(name: \"x\")\n" - " }\n" - " func callAmbiguousUpload() {\n" - " self.service.upload(Data(), to: URL(string: \"/x\")!)\n" - " }\n" - " func callWithDefault() { self.service.withDefault(message: \"x\") }\n" - " func callWithClosure() { self.service.withCallback { } }\n" - " func callRecord() { self.service.record(path: \"/audit\", session: URLSession.shared) }\n" - " func callManyWithComment() {\n" - " self.service.many(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 // trailing comment\n" - " )\n" - " }\n" - " func callHandle() { self.service.handle { } onError: { } }\n" - " func callHandleWrongLabel() { self.service.handle { } bogus: { } }\n" - " func callHandleIncomplete() { self.service.handle { } onError: { recover( } }\n" - "}\n"); + write_temp_file( + tmp, "Sources/Service.swift", + "class Service {\n" + " let sink = Sink()\n" + "\n" + " // Overload A: DOES call target()\n" + " func work(flag: Bool) {\n" + " self.sink.target()\n" + " }\n" + "\n" + " // Overload B: does NOT call target()\n" + " func work(name: String) {\n" + " print(name)\n" + " }\n" + " func upload(_ data: Data, to url: URL) { sink.target() }\n" + " func upload(_ fileURL: URL, to url: URL) { print(fileURL) }\n" + " func record(path: String, session: URLSession) {}\n" + " func withDefault(message: String, flag: Bool = true) { sink.target() }\n" + " func withCallback(completion: () -> Void) { sink.target() }\n" + " func many(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, h: Int) {}\n" + " func handle(completion: () -> Void) { sink.target() }\n" + " func handle(completion: () -> Void, onError: () -> Void) { sink.target() }\n" + "}\n"); + write_temp_file( + tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + "\n" + " // Calls ONLY overload B, which never reaches target()\n" + " func onlyCallsOverloadB() {\n" + " self.service.work(name: \"x\")\n" + " }\n" + " func callAmbiguousUpload() {\n" + " self.service.upload(Data(), to: URL(string: \"/x\")!)\n" + " }\n" + " func callWithDefault() { self.service.withDefault(message: \"x\") }\n" + " func callWithClosure() { self.service.withCallback { } }\n" + " func callRecord() { self.service.record(path: \"/audit\", session: URLSession.shared) " + "}\n" + " func callManyWithComment() {\n" + " self.service.many(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 // trailing " + "comment\n" + " )\n" + " }\n" + " func callHandle() { self.service.handle { } onError: { } }\n" + " func callHandleWrongLabel() { self.service.handle { } bogus: { } }\n" + " func callHandleIncomplete() { self.service.handle { } onError: { recover( } }\n" + "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/swiftoverload.db", tmp); @@ -8403,10 +8407,10 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { snprintf(closure_caller_qn, sizeof(closure_caller_qn), "%s.Sources.Caller.Caller.callWithClosure()", project); char record_qn[512], record_caller_qn[512], many_qn[512], many_caller_qn[512]; - snprintf(record_qn, sizeof(record_qn), "%s.Sources.Service.Service.record(path:String,session:URLSession)", + snprintf(record_qn, sizeof(record_qn), + "%s.Sources.Service.Service.record(path:String,session:URLSession)", project); + snprintf(record_caller_qn, sizeof(record_caller_qn), "%s.Sources.Caller.Caller.callRecord()", project); - snprintf(record_caller_qn, sizeof(record_caller_qn), - "%s.Sources.Caller.Caller.callRecord()", project); snprintf(many_qn, sizeof(many_qn), "%s.Sources.Service.Service.many(a:Int,b:Int,c:Int,d:Int,e:Int,f:Int,g:Int,h:Int)", project); @@ -8461,8 +8465,8 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { "%s.Sources.Service.Service.handle(completion:()=>Void)", project); snprintf(handle_two_qn, sizeof(handle_two_qn), "%s.Sources.Service.Service.handle(completion:()=>Void,onError:()=>Void)", project); - snprintf(handle_caller_qn, sizeof(handle_caller_qn), - "%s.Sources.Caller.Caller.callHandle()", project); + snprintf(handle_caller_qn, sizeof(handle_caller_qn), "%s.Sources.Caller.Caller.callHandle()", + project); snprintf(handle_wrong_qn, sizeof(handle_wrong_qn), "%s.Sources.Caller.Caller.callHandleWrongLabel()", project); snprintf(handle_incomplete_qn, sizeof(handle_incomplete_qn), @@ -8471,18 +8475,21 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { cbm_node_t handle_wrong_caller = {0}, handle_incomplete_caller = {0}; ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_one_qn, &handle_one), CBM_STORE_OK); ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_two_qn, &handle_two), CBM_STORE_OK); - ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_caller_qn, &handle_caller), CBM_STORE_OK); - ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_wrong_qn, &handle_wrong_caller), + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_caller_qn, &handle_caller), CBM_STORE_OK); - ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_incomplete_qn, &handle_incomplete_caller), + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_wrong_qn, &handle_wrong_caller), CBM_STORE_OK); + ASSERT_EQ( + cbm_store_find_node_by_qn(s, project, handle_incomplete_qn, &handle_incomplete_caller), + CBM_STORE_OK); /* `handle { } onError: { }`: only the two-closure overload can take both. */ ASSERT_EQ(pipeline_has_calls_edge(s, handle_caller.id, handle_two.id), 1); ASSERT_EQ(pipeline_has_calls_edge(s, handle_caller.id, handle_one.id), 0); /* A second closure whose label matches no parameter binds neither. */ ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong_caller.id, handle_two.id), 0); ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong_caller.id, handle_one.id), 0); - /* An incomplete second closure marks candidate input unknown; resolver must not emit a bare-name edge. */ + /* An incomplete second closure marks candidate input unknown; resolver must not emit a + * bare-name edge. */ ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete_caller.id, handle_two.id), 0); ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete_caller.id, handle_one.id), 0); cbm_node_free_fields(&handle_incomplete_caller); @@ -8525,17 +8532,18 @@ TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { " func handle(completion: () -> Void) {}\n" " func handle(completion: () -> Void, onError: () -> Void) {}\n" "}\n"); - write_temp_file(tmp, "Sources/Caller.swift", - "class Caller {\n" - " let service = Service()\n" - " func invoke() {\n" - " self.service.upload(Data(), to: URL(string: \"/x\")!)\n" - " self.service.record(path: \"/audit\", session: URLSession.shared)\n" - " }\n" - " func callHandle() { self.service.handle { } onError: { } }\n" - " func callHandleWrongLabel() { self.service.handle { } bogus: { } }\n" - " func callHandleIncomplete() { self.service.handle { } onError: { recover( } }\n" - "}\n"); + write_temp_file( + tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + " func invoke() {\n" + " self.service.upload(Data(), to: URL(string: \"/x\")!)\n" + " self.service.record(path: \"/audit\", session: URLSession.shared)\n" + " }\n" + " func callHandle() { self.service.handle { } onError: { } }\n" + " func callHandleWrongLabel() { self.service.handle { } bogus: { } }\n" + " func callHandleIncomplete() { self.service.handle { } onError: { recover( } }\n" + "}\n"); for (int i = 0; i < 50; i++) { char path[64], source[80]; snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); @@ -8634,6 +8642,88 @@ TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { PASS(); } +TEST(pipeline_swift_default_before_required_candidates_issue2061) { + for (int mode = 0; mode < 2; mode++) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_default_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " func choose(x first: Int = 0, x second: Int) {}\n" + " func choose(x first: String = \"\", x second: String) {}\n" + " func consume(_ first: Int = 0, _ second: Int) {}\n" + "}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + " func labeled() { self.service.choose(x: 1) }\n" + " func unlabeled() { self.service.consume(1) }\n" + " func wrong() { self.service.choose(y: 1) }\n" + "}\n"); + for (int i = 0; mode && i < 50; i++) { + char path[64], source[80]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(source, sizeof(source), "func filler%d() {}\n", i); + write_temp_file(tmp, path, source); + } + const char *previous = getenv("CBM_WORKERS"); + char *saved = previous ? strdup(previous) : NULL; + cbm_setenv("CBM_WORKERS", mode ? "4" : "1", 1); + char db[512]; + snprintf(db, sizeof(db), "%s/defaults.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + int result = p ? cbm_pipeline_run(p) : -1; + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + free(saved); + ASSERT_NOT_NULL(p); + ASSERT_EQ(result, 0); + const char *project = cbm_pipeline_project_name(p); + cbm_store_t *store = cbm_store_open_path(db); + ASSERT_NOT_NULL(store); + const char *targets[] = {"choose(x:Int,x:Int)", "choose(x:String,x:String)", + "consume(_:Int,_:Int)"}; + for (int i = 0; i < 3; i++) { + char caller_qn[512], target_qn[512], wrong_qn[512]; + snprintf(caller_qn, sizeof(caller_qn), "%s.Sources.Caller.Caller.%s()", project, + i < 2 ? "labeled" : "unlabeled"); + snprintf(target_qn, sizeof(target_qn), "%s.Sources.Service.Service.%s", project, + targets[i]); + snprintf(wrong_qn, sizeof(wrong_qn), "%s.Sources.Caller.Caller.wrong()", project); + cbm_node_t caller = {0}, target = {0}, wrong = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(store, project, caller_qn, &caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(store, project, target_qn, &target), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(store, project, wrong_qn, &wrong), CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(store, caller.id, target.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(store, wrong.id, target.id), 0); + cbm_edge_t *edges = NULL; + int count = 0, compatible = 0; + ASSERT_EQ( + cbm_store_find_edges_by_source_type(store, caller.id, "CALLS", &edges, &count), + CBM_STORE_OK); + for (int e = 0; e < count; e++) { + if (edges[e].target_id == target.id && edges[e].properties_json && + strstr(edges[e].properties_json, + i < 2 ? "\"candidates\":2" : "\"candidates\":1")) { + compatible++; + } + } + ASSERT_EQ(compatible, 1); + cbm_store_free_edges(edges, count); + cbm_node_free_fields(&caller); + cbm_node_free_fields(&target); + cbm_node_free_fields(&wrong); + } + cbm_store_close(store); + cbm_pipeline_free(p); + th_rmtree(tmp); + } + PASS(); +} + /* #2061: a Swift call matching NONE of a same-named project symbol's * overloads must not bind to it (a bare-name match would bind a wrong * overload), while the empty-resolution service fallbacks still apply — @@ -17707,9 +17797,9 @@ static bool swift_default_restore_graph_is_valid(cbm_store_t *store, const char cbm_store_find_node_by_qn(store, project, bad_qn, &bad) == CBM_STORE_OK; if (valid) { valid = service.name && strcmp(service.name, "ping") == 0 && service.properties_json && - strstr(service.properties_json, - has_default ? "\"swift_defaults\":\"0000000000000002\"" - : "\"swift_defaults\":\"0000000000000000\"") && + strstr(service.properties_json, has_default + ? "\"swift_defaults\":\"0000000000000002\"" + : "\"swift_defaults\":\"0000000000000000\"") && strstr(service.properties_json, "\"swift_params\":2") && pipeline_has_calls_edge(store, good.id, service.id) == has_default && pipeline_has_calls_edge(store, bad.id, service.id) == 0; @@ -18307,6 +18397,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_swift_http_call_makes_route_issue1892); RUN_TEST(pipeline_swift_overloads_keep_argument_labels_issue2061); RUN_TEST(pipeline_swift_overloads_parallel_candidates_issue2061); + RUN_TEST(pipeline_swift_default_before_required_candidates_issue2061); RUN_TEST(pipeline_swift_incompatible_overload_unresolved_issue2061); RUN_TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061); RUN_TEST(pipeline_swift_incremental_restores_default_signature_issue2061); diff --git a/tests/test_registry.c b/tests/test_registry.c index 2097eedc09..21e57887d1 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1315,16 +1315,16 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { "proj.Service.repeated(x:String,x:String)", "proj.Service.unlabeled(_:Int,_:Int)", }; - const char *names[] = {"work", "work", "work", "work", "send", "configure", - "consume", "consume", "accept", "perform", "perform", - "perform", "perform", "collect", "named", "run", "batch", - "sheet", "combine", "repeated", "repeated", "unlabeled"}; + const char *names[] = {"work", "work", "work", "work", "send", "configure", + "consume", "consume", "accept", "perform", "perform", "perform", + "perform", "collect", "named", "run", "batch", "sheet", + "combine", "repeated", "repeated", "unlabeled"}; const uint8_t counts[] = {1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 2}; for (int i = 0; i < 22; i++) { cbm_registry_add(r, names[i], qns[i], "Method"); - uint64_t defaults = (i == 5 || i == 15) ? UINT64_C(1) << 1 + uint64_t defaults = (i == 5 || i == 15) ? UINT64_C(1) << 1 : i == 17 || i >= 19 ? UINT64_C(1) - : 0; + : 0; cbm_registry_set_swift_signature(r, qns[i], defaults, counts[i]); } @@ -1418,6 +1418,11 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { call.callee_name = "batch"; ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); ASSERT_STR_EQ(out[0], qns[16]); /* variadic closure accepts trailing closure */ + call.arg_count = 1; + args[0].keyword = "completions"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[16]); /* explicit and trailing variadic closures */ + call.arg_count = 0; call.callee_name = "sheet"; ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); From 98c3a406cc06db6a2134eb2444e33cddf3ffb387 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 09:52:47 +0800 Subject: [PATCH 21/49] style(swift): format variadic matching transition Signed-off-by: DavidHLP --- src/pipeline/registry.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 8b3fba16f0..80f853ae56 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1734,8 +1734,8 @@ static bool swift_parameters_match(const swift_parameters_t *params, const swift params->variadics[i] ? swift_variadic_advance(call, arg, true) : arg + SKIP_ONE; next[end] |= states[arg]; if (params->variadics[i] && end == call->arg_count) { - next[end] |= swift_trailing_advance(params, meta, call, i, trailing_total, - states[arg]); + next[end] |= + swift_trailing_advance(params, meta, call, i, trailing_total, states[arg]); } } next[arg] |= trailing; From 44294f896d53bc513a57d4fc7c415a1ba810d12b Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 10:00:47 +0800 Subject: [PATCH 22/49] test(swift): preserve MCP observations through sanitizer teardown Signed-off-by: DavidHLP --- tests/repro/issue2061_swift_identity/mcp_driver.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/repro/issue2061_swift_identity/mcp_driver.c b/tests/repro/issue2061_swift_identity/mcp_driver.c index 2980b3221b..b162829725 100644 --- a/tests/repro/issue2061_swift_identity/mcp_driver.c +++ b/tests/repro/issue2061_swift_identity/mcp_driver.c @@ -159,6 +159,8 @@ static void request(cbm_mcp_server_t *srv, const char *project, int id, const ch } int main(int argc, char **argv) { + /* Preserve every observation if sanitizer teardown fails before exit flush. */ + setvbuf(stdout, NULL, _IOLBF, 0); if (argc != 2 || strcmp(argv[1], "issue2061-swift-identity") != 0 || !getenv("CBM_CACHE_DIR") || !getenv("CBM_ALLOWED_ROOT")) { fprintf(stderr, "usage: mcp-driver issue2061-swift-identity; set isolated " From 997cb1e8716c791c8f2f2091e11ca7057e207311 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 10:08:16 +0800 Subject: [PATCH 23/49] refactor(swift): bound matcher transition complexity Signed-off-by: DavidHLP --- src/pipeline/registry.c | 42 +++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 80f853ae56..aa183daee2 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1703,6 +1703,26 @@ static uint16_t swift_trailing_advance(const swift_parameters_t *params, return next; } +static void swift_parameter_advance(const swift_parameters_t *params, const swift_signature_t *meta, + const CBMCall *call, unsigned i, unsigned total, int arg, + uint16_t states, uint16_t *next) { + uint16_t trailing = + arg == call->arg_count ? swift_trailing_advance(params, meta, call, i, total, states) : 0; + if (params->variadics[i] || (meta->defaults & (UINT64_C(1) << i))) { + /* After explicit arguments, preserve the first eligible trailing + * binding instead of skipping it to a later one. */ + next[arg] |= states & (uint16_t)~(trailing >> SKIP_ONE); + } + if (swift_argument_label_matches(params, call, i, arg)) { + int end = params->variadics[i] ? swift_variadic_advance(call, arg, true) : arg + SKIP_ONE; + next[end] |= states; + if (params->variadics[i] && end == call->arg_count) { + next[end] |= swift_trailing_advance(params, meta, call, i, total, states); + } + } + next[arg] |= trailing; +} + static bool swift_parameters_match(const swift_parameters_t *params, const swift_signature_t *meta, const CBMCall *call) { /* Preserve the legacy bool as one unlabelled closure for manual callers. */ @@ -1713,32 +1733,14 @@ static bool swift_parameters_match(const swift_parameters_t *params, const swift /* Each argument cursor retains the reachable trailing-closure counts. * A matching label must not greedily consume an earlier default when * the same label is needed by a later required parameter. */ - uint16_t states[CBM_MAX_CALL_ARGS + SKIP_ONE] = {1}; + uint16_t states[CBM_MAX_CALL_ARGS + SKIP_ONE] = {SKIP_ONE}; for (unsigned i = 0; i < params->count; i++) { uint16_t next[CBM_MAX_CALL_ARGS + SKIP_ONE] = {0}; for (int arg = 0; arg <= call->arg_count; arg++) { if (!states[arg]) { continue; } - uint16_t trailing = - arg == call->arg_count - ? swift_trailing_advance(params, meta, call, i, trailing_total, states[arg]) - : 0; - if (params->variadics[i] || (meta->defaults & (UINT64_C(1) << i))) { - /* After explicit arguments, preserve the first eligible - * trailing binding instead of skipping it to a later one. */ - next[arg] |= states[arg] & (uint16_t)~(trailing >> SKIP_ONE); - } - if (swift_argument_label_matches(params, call, i, arg)) { - int end = - params->variadics[i] ? swift_variadic_advance(call, arg, true) : arg + SKIP_ONE; - next[end] |= states[arg]; - if (params->variadics[i] && end == call->arg_count) { - next[end] |= - swift_trailing_advance(params, meta, call, i, trailing_total, states[arg]); - } - } - next[arg] |= trailing; + swift_parameter_advance(params, meta, call, i, trailing_total, arg, states[arg], next); } memcpy(states, next, sizeof(states)); } From a03131c515f8ddabfefd69834c12e1d4606c537f Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 10:12:47 +0800 Subject: [PATCH 24/49] fix(swift): account for service signature scratch allocation Signed-off-by: DavidHLP --- internal/cbm/service_patterns.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 97e36554bd..75e61c485d 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -12,6 +12,7 @@ */ #include "service_patterns.h" #include "callable_sig.h" +#include "foundation/mem_core.h" #include #include @@ -1071,7 +1072,7 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) { char *base_qn = NULL; const char *match_qn_text = resolved_qn; if (base_len < strlen(resolved_qn)) { - base_qn = malloc(base_len + 1); + base_qn = cbm_alloc(CBM_MEM_CLASS_EXTRACT, base_len + 1); if (!base_qn) { return CBM_SVC_NONE; } @@ -1098,7 +1099,7 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) { result = p->kind; else if ((p = match_qn(match_qn_text, trpc_libraries))) result = p->kind; - free(base_qn); + cbm_free(CBM_MEM_CLASS_EXTRACT, base_qn); if (_svc_cache) { char *kdup = strdup(resolved_qn); From 0f294df8c1fc1be3b00e948150c2f54f06708cb3 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 10:14:58 +0800 Subject: [PATCH 25/49] refactor(swift): reuse signature depth tracking for closure types Signed-off-by: DavidHLP --- src/pipeline/registry.c | 141 ++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 79 deletions(-) diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index aa183daee2..e5dd2cceb7 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1479,60 +1479,80 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle /* Read labels from the signature-qualified QN. Types may contain commas in * tuples, generic arguments or function types, so only top-level commas split * parameters. The type text is used only to recognize a trailing closure. */ +typedef struct { + int parens; + int brackets; + int angles; +} swift_parameter_depth_t; + +static void swift_parameter_depth_step(swift_parameter_depth_t *depth, char ch, char previous) { + switch (ch) { + case '(': + depth->parens++; + break; + case ')': + if (depth->parens > 0) { + depth->parens--; + } + break; + case '[': + depth->brackets++; + break; + case ']': + if (depth->brackets > 0) { + depth->brackets--; + } + break; + case '<': + depth->angles++; + break; + case '>': + if (depth->angles > 0 && previous != '=') { + depth->angles--; + } + break; + default: + break; + } +} + +static bool swift_type_wraps(const char *start, const char *end) { + if (*start != '(') { + return false; + } + swift_parameter_depth_t depth = {0}; + bool comma = false; + for (const char *p = start; p < end; p++) { + swift_parameter_depth_step(&depth, *p, p == start ? '\0' : p[-SKIP_ONE]); + if (*p == ')' && depth.parens == 0) { + return p == end - SKIP_ONE && !comma; + } + if (*p == ',' && depth.parens == SKIP_ONE && depth.brackets == 0 && depth.angles == 0) { + comma = true; + } + } + return false; +} + static bool swift_type_is_closure(const char *start, const char *end) { while (start < end) { - if (end[-1] == '?') { + if (end[-SKIP_ONE] == '?') { end--; continue; } - if (*start != '(') { - break; - } - int depth = 0, brackets = 0, angles = 0; - bool wraps = false; - bool comma = false; - for (const char *p = start; p < end; p++) { - if (*p == '(') { - depth++; - } else if (*p == ')' && --depth == 0) { - wraps = p == end - 1; - break; - } else if (*p == '[') { - brackets++; - } else if (*p == ']' && brackets > 0) { - brackets--; - } else if (*p == '<') { - angles++; - } else if (*p == '>' && angles > 0 && p[-1] != '=') { - angles--; - } else if (*p == ',' && depth == 1 && brackets == 0 && angles == 0) { - comma = true; - } - } - if (!wraps || comma) { + if (!swift_type_wraps(start, end)) { break; } start++; end--; } - int parens = 0, brackets = 0, angles = 0; - for (const char *p = start; p + 1 < end; p++) { - if (*p == '=' && p[1] == '>' && parens == 0 && brackets == 0 && angles == 0) { + swift_parameter_depth_t depth = {0}; + for (const char *p = start; p + SKIP_ONE < end; p++) { + if (*p == '=' && p[SKIP_ONE] == '>' && depth.parens == 0 && depth.brackets == 0 && + depth.angles == 0) { return true; } - if (*p == '(') { - parens++; - } else if (*p == ')' && parens > 0) { - parens--; - } else if (*p == '[') { - brackets++; - } else if (*p == ']' && brackets > 0) { - brackets--; - } else if (*p == '<') { - angles++; - } else if (*p == '>' && angles > 0 && (p == start || p[-1] != '=')) { - angles--; - } + swift_parameter_depth_step(&depth, *p, p == start ? '\0' : p[-SKIP_ONE]); } return false; } @@ -1547,12 +1567,6 @@ typedef struct { unsigned count; } swift_parameters_t; -typedef struct { - int parens; - int brackets; - int angles; -} swift_parameter_depth_t; - /* Exact match for a later trailing closure's label. NULL and empty labels * never match, so a call with no captured label binds nothing by label. */ static bool swift_label_matches(const char *label, const char *want, size_t want_len) { @@ -1607,37 +1621,6 @@ static bool swift_parameter_add(swift_parameters_t *params, const char *entry, c return true; } -static void swift_parameter_depth_step(swift_parameter_depth_t *depth, char ch, char previous) { - switch (ch) { - case '(': - depth->parens++; - break; - case ')': - if (depth->parens > 0) { - depth->parens--; - } - break; - case '[': - depth->brackets++; - break; - case ']': - if (depth->brackets > 0) { - depth->brackets--; - } - break; - case '<': - depth->angles++; - break; - case '>': - if (depth->angles > 0 && previous != '=') { - depth->angles--; - } - break; - default: - break; - } -} - /* Split only at top-level commas; preserve the closure/variadic spelling rules. */ static bool swift_parameters_read(const char *qn, const char *name, const swift_signature_t *meta, swift_parameters_t *params) { From 39d5db48a83f94641fccbb44c8cbc0233b5e2a51 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 11:24:08 +0800 Subject: [PATCH 26/49] docs(swift): publish current main integration and validation evidence Signed-off-by: DavidHLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 128 ++++++++++++++++++ .../evidence/2026-10-05/README.md | 26 ++++ .../evidence/2026-10-05/final-graph.stdout | 19 +++ .../evidence/2026-10-05/final-mcp.stdout | 16 +++ .../evidence/2026-10-05/noble-cli-caller.json | 1 + .../evidence/2026-10-05/noble-cli-name.json | 1 + .../evidence/2026-10-05/noble-cli-search.json | 1 + .../evidence/2026-10-05/noble-cli-target.json | 1 + .../evidence/2026-10-05/noble-cli-tree.json | 1 + .../2026-10-05/objectscript-routine.stderr | 6 + .../2026-10-05/objectscript-udl.stderr | 6 + .../2026-10-05/swift-cli-validate.py.txt | 44 ++++++ .../2026-10-05/swift-combined-driver.c.txt | 22 +++ .../swift-objectscript-baseline-probe.c.txt | 24 ++++ 14 files changed, 296 insertions(+) create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/README.md create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-graph.stdout create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-mcp.stdout create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-caller.json create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-name.json create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-search.json create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-target.json create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-tree.json create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-routine.stderr create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-udl.stderr create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-cli-validate.py.txt create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-combined-driver.c.txt create mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-objectscript-baseline-probe.c.txt diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md index 98be82a1af..d6a5c93cf0 100644 --- a/docs/SWIFT_IDENTITY_VALIDATION.md +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -5,6 +5,134 @@ callable identity and conservative overload candidates, including candidate counts and multiple trailing closure labels. The scope remains one language, one claim. +## Current main integration — 2026-10-05 + +This section supersedes the readiness and dependency statements in the historical +snapshots below. Those results retain their original tested SHA; they are not +reruns of this delivery. The current implementation was tested at +`0f294df8c1fc1be3b00e948150c2f54f06708cb3` (code checkpoint C). +Any subsequent documentation-only commit preserves C's source inputs rather +than claiming that tests ran against the later Git metadata. + +Parent #2342 merged as `01fd6cb5e680662d55b040703d6b08b128b8e10f` and is +an ancestor of current main `268a9d8886642eb7f9b2ce45f5ce27cdecf0f519`. +Merge `251c06ac988fe96059581dadd5b901428e7164ce` integrated main into +`feat/issue-2061-swift-identity` without rewriting published history. PR #2436 +now targets main. Integration preserves upstream route-mount emission and adds +the missing emitter arguments to the Swift paths. No parent implementation is +duplicated in the main-relative diff. + +The additional regression fixes optional parameters preceding required +parameters with the same label, including unlabeled parameters. The former +greedy match could consume the required argument too early. Bounded cursor +transitions now retain every compatible binding. The existing deterministic +trailing-closure rules and variadic behavior remain in force. Serial and parallel +pipeline tests assert candidate edges and counts, including incompatible labels; +literal spelling is not used to guess overload types. Existing argument, +parameter, bucket and shared signature limits remain documented limitations; +the DAV-63 long-signature work is separate. + +### Executed checks + +All commands ran on **remote-dev**, not the local editing machine. Native host: +omarchy, GCC/G++ 16.2.1 (20260810), glibc 2.44, eight CPUs. Evidence root: +`/home/david/swift-delivery-20261005`. Logs, exit files, source provenance and +executables are retained there. No sanitizer suppression was added. + +| Check at C | Command / evidence prefix | Result and limit | +| --- | --- | --- | +| Focused suites | `scripts/test.sh --suites extraction,callable_sig,registry,pipeline,index_format`; `final-code-focused` | **825 passed, exit 0**; two baseline ObjectScript UBSan diagnostics, so this aggregate is not sanitizer-clean | +| Original graph reproduction | combined driver `graph .../final-graph`; `final-graph` | **10/10, exit 0**, ASan/UBSan/LSan clean; fresh production pipeline/store graph | +| MCP presentation | combined driver `mcp issue2061-swift-identity`; `final-mcp` | **5/5, exit 0**, ASan/UBSan/LSan clean; all JSON envelopes and the exact text tree observed | +| Pipeline TSan | `scripts/test.sh --tsan TEST_TSAN_SUITES=pipeline`; `final-tsan-pipeline` | **325 passed, exit 0**, including Swift serial/parallel overload, default and trailing-closure regressions; no TSan report | +| Default broad TSan | `timeout 3600 scripts/test.sh --tsan`; `final-tsan` | **exit 124**, timed out in daemon-runtime suite before pipeline; not a completed broad pass | +| Native production build | `scripts/build.sh BUILD_DIR=build/swift-production`; `final-production-build` | **exit 0**; native CLI startup fails before main in mimalloc/C++ locale initialization | +| Ubuntu production build | `scripts/build.sh BUILD_DIR=build/swift-noble-prod`; `noble-production-build2` | **exit 0**, standard production allocator/flags; source mounted read-only | +| Production CLI | index plus five queries; `noble-cli-*` | **index exit 0, five observations pass**, distinct signature display, counts, correct paths and exact text tree | +| Full canonical entry | `scripts/test.sh`; `final-code-full-test` | **exit 1 at Step0x**, same Scoop newest-release metadata contract failure on main; dynamic suites/production guards not reached | +| Full lint | `scripts/lint.sh`; intermediate `final-lint` and `baseline-lint` logs | **exit 2**, baseline clang-tidy diagnostics; final registry analysis has no diagnostics on main-relative added lines | +| CI-mode lint | `timeout 900 scripts/lint.sh --ci`; `final-code-ci-lint` | memory-core check passes; **exit 124** during cppcheck, full lint acceptance incomplete | + +The baseline attribution is deliberately bounded. Main's ObjectScript UDL and +routine scanner sources are byte-identical to C. Independent ASan/UBSan probes +at main `268a9d88` reproduce both null-buffer/zero-length deserialize diagnostics +at scanner.c lines 122 and 118, with `UBSAN_OPTIONS=halt_on_error=1`, each exit 1 +(`objectscript-udl` / `objectscript-routine`). This is a direct baseline control, +not a claim that main's entire focused suite was rerun. These unrelated vendored +scanners were not changed. + +The regression's semantic RED checkpoint is +`233f5693a4302065ab8a680ce5c507aed95cec96`: integrated build fixes and the +new test, but the old greedy matcher. `scripts/test.sh --suites registry` fails +at test_registry.c:1369 (candidate count -1 instead of 2), with 78 passes and +one failure. The assertion exits before registry cleanup, producing an LSan +report of 77,323 bytes in 108 allocations. This is a failing RED run, not a clean +sanitizer run. The earlier `b4e3daf0` attempt failed compilation and is not the +semantic RED proof. + +For the original reproduction, an external launcher only renames the two +existing drivers' main functions and graph counters, then dispatches `graph` +or `mcp`. It compiles the unchanged production sources once through: + +```bash +make -j4 -f Makefile.cbm build/c/test-runner \ + ALL_TEST_SRCS=/home/david/swift-delivery-20261005/swift-combined-driver.c +ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ +UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ +timeout 180 build/c/test-runner graph "$evidence/final-graph" +# After the graph writer closes, SQLite's backup API creates a fresh private +# cache/issue2061-swift-identity.db from the persisted graph, without overwriting. +CBM_CACHE_DIR="$evidence/mcp-cache" CBM_ALLOWED_ROOT="$evidence/final-graph/repo" \ +CBM_RUNTIME_DIR="$evidence/mcp-runtime" \ +ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ +UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ +timeout 180 build/c/test-runner mcp issue2061-swift-identity +``` + +The fresh graph has two stored bare-name `work` nodes at lines 5–7 and 10–12, +with label/type-qualified names. Only `work(flag:Bool)` calls target; the caller +reaches only `work(name:String)`; target's depth-three inbound traversal has no +false caller. MCP and CLI independently confirm signature-bearing display +leaves, exact totals and paths. Stored bare names and displayed qualified-name +leaves are separate contracts. Persistence, metadata restoration and index-format +rebuild behavior are additionally covered by the current focused suites. + +Native startup is an environment failure: a minimal no-Swift C++ program linked +with the exact production mimalloc object also crashes before main on glibc2.44. +The same object/probe enters main successfully in the existing remote Docker +image `codebase-memory-clang-tidy21:swift2061-noble-v1`, ID +`sha256:e24dbbd5c42fea34b839fc435d7110b9c52263ce00378299a956950d6184725a`. +This verified Ubuntu24.04.5/GCC13.3/glibc2.39 environment was therefore used for +the actual production CLI, with network disabled and private cache/runtime. +Git is absent inside this image, so build version metadata falls back to dev; +host Git checks establish source C. The first container build stopped before +compilation because Git was absent; only the second completed build is a pass. + +CLI commands are `cli --json index_repository --repo-path /evidence/cli-fixture +--mode fast --name issue2061-swift-identity`, `search_graph --name-pattern +'^work$' --format json --limit 20`, and `trace_path` for target inbound depth3, +onlyCallsOverloadB outbound depth1, and the fully qualified name overload +outbound depth1 (JSON, limit100, max-output-tokens3200). A fifth query requests +target's default text tree. All use project `issue2061-swift-identity`. + +| Retained artifact | SHA-256 | +| --- | --- | +| Focused runner | `f0ca3564c49e7424336c2d336c10d9faaba6e160cf320969458e78968e1722cb` | +| Focused log | `e32de50335b9016144df5ac103ed8d51a46ca2624e1c792e1a3c380565b9c83f` | +| Combined launcher | `d920968ed9f19b265e806945485c3143969ec951fe5e9744ef5612956ce36f57` | +| Combined driver executable | `24addf3107f959469b377d56732bcd81703e5c0d8b6b2d94d8cab7358c2174ad` | +| Graph stdout | `8c1cb416f731229c684d058c184f7ed86d1b7fa986c35b4e006da08a133ce661` | +| MCP stdout | `cebbf9a59dcd5de21118d56adb85a50228ca99f885c2b2522d1177d1095a3efd` | +| CLI validator | `54c640bb3f28d912822fd11e73103706984d49a9a33ba812d1a86e1dbd454bb3` | +| Ubuntu production executable | `c6aac37edc7efb39b1cd9ab4bf902675c6fa882110fc98fa8dccd6717cb5042d` | + +GitHub CI/DCO/CodeQL for C remain queued/pending at the publication checkpoint; +the earlier starting-head DCO success is historical. Required checks and +maintainer approval are not supplied by local evidence. Upstream review and +merge remain external; no branch-protection bypass or approval substitution +was attempted. DAV-61 is independently Done; DAV-62 remains future multilingual +work. The rest of this document is historical evidence. + ## Tested source The A-only checkpoint contains the seven Swift implementation and regression diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/README.md b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/README.md new file mode 100644 index 0000000000..77c82b4abc --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/README.md @@ -0,0 +1,26 @@ +# Current delivery observations + +Tested production/test source: `0f294df8c1fc1be3b00e948150c2f54f06708cb3`. +These are actual remote-dev outputs, not expected-output fixtures. See +[the validation record](../../../../../docs/SWIFT_IDENTITY_VALIDATION.md#current-main-integration--2026-10-05) +for commands, environments, exit codes, baseline attribution and limitations. + +- `final-graph.stdout`: ten original reproduction observations, exit 0. +- `final-mcp.stdout`: all five JSON-RPC requests/responses and assertions, exit 0. +- `noble-cli-*.json`: five separate production CLI envelopes, validator passes. +- `objectscript-*.stderr`: strict UBSan probes against main `268a9d88`, each exit 1. + +The `.txt` source records are the exact external launchers/probes used on the +remote host. They are excluded from automatic suite discovery. To reproduce, +copy them to the original external evidence paths (dropping `.txt`) and adjust +the absolute checkout paths if needed. The combined launcher includes the two +existing production-backed drivers unchanged, using separate process runs. +The CLI validator examines the saved envelopes rather than bypassing the CLI. +The ObjectScript probe compiles only main's two unchanged scanners, with +`-std=c11 -D_DEFAULT_SOURCE -g -O1 -fsanitize=address,undefined +-fno-omit-frame-pointer` and their grammar include directories. It establishes +the specific baseline defect, not full main test acceptance. + +Current graph/MCP runs use strict ASan/UBSan and leak detection and report none. +This does not erase the historical MCP failure or the two unrelated UBSan +reports in the aggregate focused test run. diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-graph.stdout b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-graph.stdout new file mode 100644 index 0000000000..2ff4ab5e58 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-graph.stdout @@ -0,0 +1,19 @@ +EVIDENCE root=/home/david/swift-delivery-20261005/final-graph mode=fast +NODE name=work id=12 file=Sources/Service.swift lines=5-7 qn=issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) +NODE name=work id=13 file=Sources/Service.swift lines=10-12 qn=issue2061-swift-identity.Sources.Service.Service.work(name:String) +NODE name=work id=12 file=Sources/Service.swift lines=5-7 qn=issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) +NODE name=work id=13 file=Sources/Service.swift lines=10-12 qn=issue2061-swift-identity.Sources.Service.Service.work(name:String) +NODE name=target id=9 file=Sources/Sink.swift lines=2-2 qn=issue2061-swift-identity.Sources.Sink.Sink.target() +NODE name=onlyCallsOverloadB id=17 file=Sources/Caller.swift lines=5-7 qn=issue2061-swift-identity.Sources.Caller.Caller.onlyCallsOverloadB() +OBS two_work_nodes actual=2 expected=2 PASS +OBS flag_node_at_5_7 actual=1 expected=1 PASS +OBS name_node_at_10_12 actual=1 expected=1 PASS +OBS target_node actual=1 expected=1 PASS +OBS caller_node actual=1 expected=1 PASS +OBS flag_calls_target actual=1 expected=1 PASS +OBS name_does_not_call_target actual=0 expected=0 PASS +OBS caller_calls_name actual=1 expected=1 PASS +OBS caller_does_not_call_flag actual=0 expected=0 PASS +INBOUND remaining_depth=3 source=12 target=9 +OBS target_inbound_depth3_has_no_false_caller actual=0 expected=0 PASS +SUMMARY passed=10 failed=0 errors=0 exit=0 diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-mcp.stdout b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-mcp.stdout new file mode 100644 index 0000000000..9946e157b8 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-mcp.stdout @@ -0,0 +1,16 @@ +REQUEST 1 {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_graph","arguments":{"project":"issue2061-swift-identity","name_pattern":"^work$","format":"json","limit":20}}} +RAW 1 {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"label\",\"lines\",\"in\",\"out\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"file\":\"Sources/Service.swift\",\"rows\":[[\"work(flag:Bool)\",\"Method\",\"5-7\",0,2],[\"work(name:String)\",\"Method\",\"10-12\",1,0]]}],\"total\":2,\"returned\":2,\"count\":2,\"has_more\":false,\"truncated\":false}"}],"structuredContent":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","label","lines","in","out"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","file":"Sources/Service.swift","rows":[["work(flag:Bool)","Method","5-7",0,2],["work(name:String)","Method","10-12",1,0]]}],"total":2,"returned":2,"count":2,"has_more":false,"truncated":false},"isError":false}} +OBS request_1 PASS +REQUEST 2 {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"trace_path","arguments":{"project":"issue2061-swift-identity","function_name":"target","direction":"inbound","depth":3,"limit":100,"max_output_tokens":3200,"format":"json"}}} +RAW 2 {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"function\":\"target\",\"direction\":\"inbound\",\"callers_total\":1,\"callers_total_relation\":\"eq\",\"callers\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"rows\":[[\"work(flag:Bool)\",1]]}]}}"}],"structuredContent":{"function":"target","direction":"inbound","callers_total":1,"callers_total_relation":"eq","callers":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","rows":[["work(flag:Bool)",1]]}]}},"isError":false}} +OBS request_2 PASS +REQUEST 3 {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"trace_path","arguments":{"project":"issue2061-swift-identity","function_name":"target","direction":"inbound","depth":3,"limit":100,"max_output_tokens":3200}}} +RAW 3 {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"function: target\ndirection: inbound\ncallers_total: 1\ncallers_total_relation: eq\ncallers: 1 (cols: qn hop)\n issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) 1\n"}],"isError":false}} +OBS request_3 PASS +REQUEST 4 {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"trace_path","arguments":{"project":"issue2061-swift-identity","function_name":"onlyCallsOverloadB","direction":"outbound","depth":1,"limit":100,"max_output_tokens":3200,"format":"json"}}} +RAW 4 {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"{\"function\":\"onlyCallsOverloadB\",\"direction\":\"outbound\",\"callees_total\":1,\"callees_total_relation\":\"eq\",\"callees\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"rows\":[[\"work(name:String)\",1]]}]}}"}],"structuredContent":{"function":"onlyCallsOverloadB","direction":"outbound","callees_total":1,"callees_total_relation":"eq","callees":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","rows":[["work(name:String)",1]]}]}},"isError":false}} +OBS request_4 PASS +REQUEST 5 {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"trace_path","arguments":{"project":"issue2061-swift-identity","function_name":"issue2061-swift-identity.Sources.Service.Service.work(name:String)","direction":"outbound","depth":1,"limit":100,"max_output_tokens":3200,"format":"json"}}} +RAW 5 {"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"{\"function\":\"issue2061-swift-identity.Sources.Service.Service.work(name:String)\",\"direction\":\"outbound\",\"callees_total\":0,\"callees_total_relation\":\"eq\",\"callees\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[]}}"}],"structuredContent":{"function":"issue2061-swift-identity.Sources.Service.Service.work(name:String)","direction":"outbound","callees_total":0,"callees_total_relation":"eq","callees":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[]}},"isError":false}} +OBS request_5 PASS +SUMMARY passed=5 failed=0 errors=0 exit=0 diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-caller.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-caller.json new file mode 100644 index 0000000000..6234bbac65 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-caller.json @@ -0,0 +1 @@ +{"content":[{"type":"text","text":"{\"function\":\"onlyCallsOverloadB\",\"direction\":\"outbound\",\"callees_total\":1,\"callees_total_relation\":\"eq\",\"callees\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"rows\":[[\"work(name:String)\",1]]}]}}"}],"structuredContent":{"function":"onlyCallsOverloadB","direction":"outbound","callees_total":1,"callees_total_relation":"eq","callees":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","rows":[["work(name:String)",1]]}]}},"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-name.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-name.json new file mode 100644 index 0000000000..43bf3fda5b --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-name.json @@ -0,0 +1 @@ +{"content":[{"type":"text","text":"{\"function\":\"issue2061-swift-identity.Sources.Service.Service.work(name:String)\",\"direction\":\"outbound\",\"callees_total\":0,\"callees_total_relation\":\"eq\",\"callees\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[]}}"}],"structuredContent":{"function":"issue2061-swift-identity.Sources.Service.Service.work(name:String)","direction":"outbound","callees_total":0,"callees_total_relation":"eq","callees":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[]}},"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-search.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-search.json new file mode 100644 index 0000000000..3083b8c003 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-search.json @@ -0,0 +1 @@ +{"content":[{"type":"text","text":"{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"label\",\"lines\",\"in\",\"out\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"file\":\"Sources/Service.swift\",\"rows\":[[\"work(flag:Bool)\",\"Method\",\"5-7\",0,2],[\"work(name:String)\",\"Method\",\"10-12\",1,0]]}],\"total\":2,\"returned\":2,\"count\":2,\"has_more\":false,\"truncated\":false}"}],"structuredContent":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","label","lines","in","out"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","file":"Sources/Service.swift","rows":[["work(flag:Bool)","Method","5-7",0,2],["work(name:String)","Method","10-12",1,0]]}],"total":2,"returned":2,"count":2,"has_more":false,"truncated":false},"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-target.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-target.json new file mode 100644 index 0000000000..290e5f4c50 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-target.json @@ -0,0 +1 @@ +{"content":[{"type":"text","text":"{\"function\":\"target\",\"direction\":\"inbound\",\"callers_total\":1,\"callers_total_relation\":\"eq\",\"callers\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"rows\":[[\"work(flag:Bool)\",1]]}]}}"}],"structuredContent":{"function":"target","direction":"inbound","callers_total":1,"callers_total_relation":"eq","callers":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","rows":[["work(flag:Bool)",1]]}]}},"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-tree.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-tree.json new file mode 100644 index 0000000000..08e536bb6f --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-tree.json @@ -0,0 +1 @@ +{"content":[{"type":"text","text":"function: target\ndirection: inbound\ncallers_total: 1\ncallers_total_relation: eq\ncallers: 1 (cols: qn hop)\n issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) 1\n"}],"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-routine.stderr b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-routine.stderr new file mode 100644 index 0000000000..8373071e13 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-routine.stderr @@ -0,0 +1,6 @@ +internal/cbm/vendored/grammars/objectscript_routine/scanner.c:118:3: runtime error: null pointer passed as argument 2, which is declared to never be null + #0 0x5ff27e837c8d in tree_sitter_objectscript_routine_external_scanner_deserialize internal/cbm/vendored/grammars/objectscript_routine/scanner.c:118 + #1 0x5ff27e81c3bf in main /home/david/swift-delivery-20261005/swift-objectscript-baseline-probe.c:20 + #2 0x74bfde427780 (/usr/lib/libc.so.6+0x27780) (BuildId: 503200d7fda94a5dc6058d7e0694e5d1dcb2e372) + #3 0x74bfde4278b8 in __libc_start_main (/usr/lib/libc.so.6+0x278b8) (BuildId: 503200d7fda94a5dc6058d7e0694e5d1dcb2e372) + #4 0x5ff27e81c214 in _start (/home/david/swift-delivery-20261005/objectscript-baseline-probe+0x2d214) (BuildId: b680da9817d0215b62060fffff7da5fb7784794c) diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-udl.stderr b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-udl.stderr new file mode 100644 index 0000000000..ab66966c06 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-udl.stderr @@ -0,0 +1,6 @@ +internal/cbm/vendored/grammars/objectscript_udl/scanner.c:122:3: runtime error: null pointer passed as argument 2, which is declared to never be null + #0 0x628280053d52 in tree_sitter_objectscript_udl_external_scanner_deserialize internal/cbm/vendored/grammars/objectscript_udl/scanner.c:122 + #1 0x628280046364 in main /home/david/swift-delivery-20261005/swift-objectscript-baseline-probe.c:16 + #2 0x7f5316627780 (/usr/lib/libc.so.6+0x27780) (BuildId: 503200d7fda94a5dc6058d7e0694e5d1dcb2e372) + #3 0x7f53166278b8 in __libc_start_main (/usr/lib/libc.so.6+0x278b8) (BuildId: 503200d7fda94a5dc6058d7e0694e5d1dcb2e372) + #4 0x628280046214 in _start (/home/david/swift-delivery-20261005/objectscript-baseline-probe+0x2d214) (BuildId: b680da9817d0215b62060fffff7da5fb7784794c) diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-cli-validate.py.txt b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-cli-validate.py.txt new file mode 100644 index 0000000000..248acdfcae --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-cli-validate.py.txt @@ -0,0 +1,44 @@ +import json +from pathlib import Path + +root = Path('/home/david/swift-delivery-20261005') +results = {} +for name in ('search', 'target', 'caller', 'name', 'tree'): + envelope = json.loads((root / f'noble-cli-{name}.json').read_text()) + assert envelope['isError'] is False, name + content = envelope['content'] + assert len(content) == 1 and content[0]['type'] == 'text', name + if name != 'tree': + data = json.loads(content[0]['text']) + assert envelope['structuredContent'] == data, name + results[name] = data + else: + expected = ('function: target\ndirection: inbound\ncallers_total: 1\n' + 'callers_total_relation: eq\ncallers: 1 (cols: qn hop)\n' + ' issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) 1\n') + assert content[0]['text'] == expected + assert 'structuredContent' not in envelope +search = results['search'] +assert search['total'] == search['returned'] == search['count'] == 2 +assert search['has_more'] is False and search['truncated'] is False +assert search['cols'] == ['name', 'label', 'lines', 'in', 'out'] +assert len(search['groups']) == 1 +group = search['groups'][0] +assert group['qn_prefix'] == 'issue2061-swift-identity.Sources.Service.Service' +assert group['file'] == 'Sources/Service.swift' +assert sorted(group['rows']) == [['work(flag:Bool)', 'Method', '5-7', 0, 2], + ['work(name:String)', 'Method', '10-12', 1, 0]] +for name, direction, total, leaf in (('target', 'callers', 1, 'work(flag:Bool)'), + ('caller', 'callees', 1, 'work(name:String)'), + ('name', 'callees', 0, None)): + data = results[name] + assert data[f'{direction}_total'] == total + assert data[f'{direction}_total_relation'] == 'eq' + assert data[direction]['cols'] == ['name', 'hop'] + groups = data[direction]['groups'] + if total: + assert len(groups) == 1 and groups[0]['qn_prefix'] == group['qn_prefix'] + assert groups[0]['rows'] == [[leaf, 1]] + else: + assert groups == [] +print('CLI observations: 5/5 PASS (JSON envelopes, signature display, paths/counts, tree)') diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-combined-driver.c.txt b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-combined-driver.c.txt new file mode 100644 index 0000000000..4ee4c2d984 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-combined-driver.c.txt @@ -0,0 +1,22 @@ +#define main graph_main +#define passed graph_passed +#define failed graph_failed +#define errors graph_errors +#include "/home/david/Projects/codebase-memory-mcp/tests/repro/issue2061_swift_identity/main.c" +#undef main +#undef passed +#undef failed +#undef errors +#define main mcp_main +#include "/home/david/Projects/codebase-memory-mcp/tests/repro/issue2061_swift_identity/mcp_driver.c" +#undef main + +int main(int argc, char **argv) { + if (argc != 3) + return 2; + if (strcmp(argv[1], "graph") == 0) + return graph_main(argc - 1, argv + 1); + if (strcmp(argv[1], "mcp") == 0) + return mcp_main(argc - 1, argv + 1); + return 2; +} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-objectscript-baseline-probe.c.txt b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-objectscript-baseline-probe.c.txt new file mode 100644 index 0000000000..fcfbac6a89 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-objectscript-baseline-probe.c.txt @@ -0,0 +1,24 @@ +#include +#include + +void *tree_sitter_objectscript_udl_external_scanner_create(void); +void tree_sitter_objectscript_udl_external_scanner_deserialize(void *, const char *, unsigned); +void tree_sitter_objectscript_udl_external_scanner_destroy(void *); +void *tree_sitter_objectscript_routine_external_scanner_create(void); +void tree_sitter_objectscript_routine_external_scanner_deserialize(void *, const char *, unsigned); +void tree_sitter_objectscript_routine_external_scanner_destroy(void *); + +int main(int argc, char **argv) { + if (argc != 2) + return 2; + if (strcmp(argv[1], "udl") == 0) { + void *p = tree_sitter_objectscript_udl_external_scanner_create(); + tree_sitter_objectscript_udl_external_scanner_deserialize(p, NULL, 0); + tree_sitter_objectscript_udl_external_scanner_destroy(p); + } else { + void *p = tree_sitter_objectscript_routine_external_scanner_create(); + tree_sitter_objectscript_routine_external_scanner_deserialize(p, NULL, 0); + tree_sitter_objectscript_routine_external_scanner_destroy(p); + } + return 0; +} From 7188414d0a301574ed26a95c23159f06b0749cf7 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 13:39:30 +0800 Subject: [PATCH 27/49] test(swift): expose generic and async identity collisions Signed-off-by: DavidHLP --- tests/test_callable_sig.c | 17 +++++- tests/test_pipeline.c | 122 +++++++++++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index 0c65c1a340..b33dea28ad 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -122,7 +122,7 @@ static const sig_case_t k_cases[] = { "HTTPMethod = .post, headers: [String: String]? = nil, handler: @escaping (Int) -> Void, " "xs: Int..., io: inout Swift.Int, t: (a: Int, String)) -> UploadRequest { }\n}", "function_declaration", 0, - "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(Int)=>Void," + "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(Int)=>Void," "xs:Int~,io:inout Int,t:(a:Int,String))"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class Session {\n func upload(_ data: Data, with request: URLRequest) {}\n}", @@ -136,6 +136,21 @@ static const sig_case_t k_cases[] = { "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, "(l:C,r:C)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func extra() {}", "function_declaration", 0, "()"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", + "function_declaration", 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", + "function_declaration", 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T) where T: Equatable {}", "function_declaration", 0, + "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T) where T.Element == Int {}", "function_declaration", 0, + "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f < T > ( _ x : T ) where T . Element == Int { }", "function_declaration", 0, + "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: Int) async {}", + "function_declaration", 0, "(_:Int)async"}, /* Scala: every clause flattened, by-name and repeated parameters. */ {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, "class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: " diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 1a1dcbcc75..2189b315f8 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8519,6 +8519,113 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { PASS(); } +/* Distinct declarations retain their own source ranges and outgoing edges; + * labels/arity alone cannot choose generic constraints or sync vs async. */ +TEST(pipeline_swift_generic_async_identity_issue2061) { + const char *suffixes[] = {"pick(_:T)", "pick(_:T)", + "pick(_:T)", + "pick(_:T)", "pick(_:Int)", + "pick(_:Int)async"}; + for (int mode = 0; mode < 2; mode++) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_generic_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + "func pick(_ x: T) { mark0() }\n" + "func pick(_ x: T) { mark1() }\n" + "func pick(_ x: T) where T: Hashable { mark2() }\n" + "func pick(_ x: T) where T: Comparable { mark3() }\n" + "func pick(_ x: Int) { mark4() }\n" + "func pick(_ x: Int) async { mark5() }\n" + "}\n" + "func mark0() {}\nfunc mark1() {}\nfunc mark2() {}\n" + "func mark3() {}\nfunc mark4() {}\nfunc mark5() {}\n"); + const char *caller_source = "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(value) }\n}\n"; + write_temp_file(tmp, "Sources/Caller.swift", caller_source); + for (int i = 0; mode && i < 50; i++) { + char path[64], source[80]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(source, sizeof(source), "func filler%d() {}\n", i); + write_temp_file(tmp, path, source); + } + const char *previous = getenv("CBM_WORKERS"); + char *saved = previous ? strdup(previous) : NULL; + cbm_setenv("CBM_WORKERS", mode ? "4" : "1", 1); + char db[512], project[512]; + snprintf(db, sizeof(db), "%s/generic.db", tmp); + for (int phase = 0; phase < 2; phase++) { + if (phase) { + char path[512]; + snprintf(path, sizeof(path), "%s/Sources/Caller.swift", tmp); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(value) }\n}\n// changed\n"); + ASSERT_EQ(pipeline_test_set_mtime(path, 2000000000, 0), 0); + } + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); + cbm_store_t *s = cbm_store_open_path(db); + ASSERT_NOT_NULL(s); + cbm_node_t *overloads = NULL; + int count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "pick", &overloads, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 6); + cbm_store_free_nodes(overloads, count); + char qn[512]; + snprintf(qn, sizeof(qn), "%s.Sources.Caller.Caller.invoke()", project); + cbm_node_t caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &caller), CBM_STORE_OK); + for (int i = 0; i < 6; i++) { + cbm_node_t target = {0}, marker = {0}; + snprintf(qn, sizeof(qn), "%s.Sources.Service.Service.%s", project, suffixes[i]); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &target), CBM_STORE_OK); + ASSERT_STR_EQ(target.name, "pick"); + ASSERT_EQ(target.start_line, i + 2); + ASSERT_EQ(target.end_line, i + 2); + snprintf(qn, sizeof(qn), "%s.Sources.Service.mark%d()", project, i); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &marker), CBM_STORE_OK); + cbm_edge_t *edges = NULL; + int n = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, target.id, "CALLS", &edges, &n), + CBM_STORE_OK); + ASSERT_EQ(n, 1); + ASSERT_EQ(edges[0].target_id, marker.id); + cbm_store_free_edges(edges, n); + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &n), + CBM_STORE_OK); + ASSERT_EQ(n, 6); + int compatible = 0; + for (int e = 0; e < n; e++) { + if (edges[e].target_id == target.id) { + ASSERT_NOT_NULL(strstr(edges[e].properties_json, "\"candidates\":6")); + compatible++; + } + } + ASSERT_EQ(compatible, 1); + cbm_store_free_edges(edges, n); + cbm_node_free_fields(&target); + cbm_node_free_fields(&marker); + } + cbm_node_free_fields(&caller); + cbm_store_close(s); + cbm_pipeline_free(p); + } + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + free(saved); + th_rmtree(tmp); + } + PASS(); +} + /* The parallel call pass must retain both type-only overload candidates. */ TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { char tmp[256]; @@ -8775,7 +8882,12 @@ TEST(pipeline_swift_incompatible_overload_unresolved_issue2061) { snprintf(wrong_qn, sizeof(wrong_qn), "%s.Sources.Caller.Caller.wrongLabel()", project); cbm_node_t wrong = {0}; ASSERT_EQ(cbm_store_find_node_by_qn(s, project, wrong_qn, &wrong), CBM_STORE_OK); - ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, wrong.id), 0); + cbm_node_t *gets = NULL; + int get_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "get", &gets, &get_count), CBM_STORE_OK); + ASSERT_EQ(get_count, 1); + ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 0); + cbm_store_free_nodes(gets, get_count); cbm_node_free_fields(&wrong); /* ... but the call never binds to it ... */ ASSERT_EQ(pipeline_has_calls_edge(s, runs[0].id, fetches[0].id), 0); @@ -8852,7 +8964,12 @@ TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061) { snprintf(wrong_qn, sizeof(wrong_qn), "%s.Sources.Caller.Caller.wrongLabel()", project); cbm_node_t wrong = {0}; ASSERT_EQ(cbm_store_find_node_by_qn(s, project, wrong_qn, &wrong), CBM_STORE_OK); - ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, wrong.id), 0); + cbm_node_t *gets = NULL; + int get_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "get", &gets, &get_count), CBM_STORE_OK); + ASSERT_EQ(get_count, 1); + ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 0); + cbm_store_free_nodes(gets, get_count); cbm_node_free_fields(&wrong); ASSERT_EQ(pipeline_has_calls_edge(s, runs[0].id, fetches[0].id), 0); cbm_store_free_nodes(fetches, fetch_count); @@ -18397,6 +18514,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_swift_http_call_makes_route_issue1892); RUN_TEST(pipeline_swift_overloads_keep_argument_labels_issue2061); RUN_TEST(pipeline_swift_overloads_parallel_candidates_issue2061); + RUN_TEST(pipeline_swift_generic_async_identity_issue2061); RUN_TEST(pipeline_swift_default_before_required_candidates_issue2061); RUN_TEST(pipeline_swift_incompatible_overload_unresolved_issue2061); RUN_TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061); From e1eb929fda286c22256285fdb44f9770cadb6b09 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 13:49:07 +0800 Subject: [PATCH 28/49] fix(swift): retain generic requirements and async callable identity Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 48 ++++++++++++++++++++- internal/cbm/callable_sig.h | 3 ++ src/pipeline/registry.c | 28 +++++++++++++ src/store/store.h | 2 +- tests/test_callable_sig.c | 5 +++ tests/test_index_format.c | 84 +++++++++++++++++++++++++++++++++++++ tests/test_registry.c | 10 +++++ 7 files changed, 178 insertions(+), 2 deletions(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 732345f3cc..b08e8f7ebc 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -779,6 +779,36 @@ static void sig_swift_params(sig_ctx_t *c, TSNode node) { } } +/* Keep generic declarations and requirements in identity, without interpreting + * applicability at call sites. '/' preserves associated-type paths without + * introducing a QN separator; comments and whitespace use the token rules. */ +static void sig_swift_tparams(sig_ctx_t *c, TSNode node) { + TSNode params = cbm_find_child_by_kind(node, "type_parameters"); + TSNode requirements = cbm_find_child_by_kind(node, "type_constraints"); + if (ts_node_is_null(params) && ts_node_is_null(requirements)) { + return; + } + size_t start = c->len; + if (!ts_node_is_null(params)) { + sig_type_tokens(c, params); + if (c->len > start && c->buf[c->len - 1] == '>') { + c->buf[--c->len] = '\0'; + } + } else { + sig_raw(c, "<", 1); + } + if (!ts_node_is_null(requirements)) { + sig_raw(c, ";", 1); + sig_type_tokens(c, requirements); + } + sig_raw(c, ">", 1); + for (size_t i = start; i < c->len; i++) { + if (c->buf[i] == '.') { + c->buf[i] = '/'; + } + } +} + uint64_t cbm_swift_default_mask(TSNode node, const char *source, uint8_t *count) { (void)source; uint64_t defaults = 0; @@ -1002,6 +1032,8 @@ static void sig_render(sig_ctx_t *c, TSNode node) { } if (c->lang == CBM_LANG_CSHARP) { sig_cs_tparams(c, node); + } else if (c->lang == CBM_LANG_SWIFT) { + sig_swift_tparams(c, node); } c->open_off = c->len; sig_raw(c, "(", 1); @@ -1030,6 +1062,17 @@ static void sig_render(sig_ctx_t *c, TSNode node) { } sig_raw(c, ")", 1); c->close_off = c->len; + if (c->lang == CBM_LANG_SWIFT) { + /* Only direct declaration children: a parameter's async closure type + * is already in params and must not mark the enclosing function. */ + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; i++) { + if (sig_node_text_is(c, ts_node_child(node, i), "async")) { + sig_raw(c, "async", 5); + break; + } + } + } } static bool sig_roundtrips(const char *suffix, size_t n) { @@ -1170,6 +1213,9 @@ size_t cbm_qn_callable_base_len(const char *qn) { size_t len = strlen(qn); /* Trailing cvref (C++): const / volatile / & / && after the ')'. */ size_t t = len; + if (t >= 5 && memcmp(qn + t - 5, "async", 5) == 0) { + t -= 5; + } for (;;) { if (t > 0 && qn[t - 1] == '&') { t--; @@ -1215,7 +1261,7 @@ size_t cbm_qn_callable_base_len(const char *qn) { size_t q = p; while (q > 0) { q--; - if (qn[q] == '>') { + if (qn[q] == '>' && (q == 0 || qn[q - 1] != '=')) { adepth++; } else if (qn[q] == '<') { adepth--; diff --git a/internal/cbm/callable_sig.h b/internal/cbm/callable_sig.h index 6dd62f5216..f41feaa572 100644 --- a/internal/cbm/callable_sig.h +++ b/internal/cbm/callable_sig.h @@ -4,6 +4,9 @@ * A callable's graph identity is * * callable_qn := base_qn [] (params) [cvref] + * Swift includes generic declarations and where requirements in + * (associated-type paths use '/' instead of '.') and appends function-level + * async after params. These distinguish declarations, not call applicability. * * so overloads that share a base QN (Java/C#/C++/Kotlin/Swift/Scala/ObjC) * become distinct nodes instead of merging into one node that collects every diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index e5dd2cceb7..14eb66a513 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1627,6 +1627,34 @@ static bool swift_parameters_read(const char *qn, const char *name, const swift_ size_t base_len = cbm_qn_callable_base_len_named(qn, name); const char *sig = qn + base_len; size_t n = strlen(sig); + if (n >= 5 && memcmp(sig + n - 5, "async", 5) == 0) { + n -= 5; + } + /* Declaration generic requirements distinguish nodes, but do not prove + * applicability. Read only params when selecting compatible candidates. */ + if (n && sig[0] == '<') { + unsigned depth = 0; + size_t i = 0; + for (; i < n; i++) { + if (sig[i] == '<') { + depth++; + } else if (sig[i] == '>' && (i == 0 || sig[i - 1] != '=')) { + if (depth == 0) { + return false; + } + if (--depth != 0) { + continue; + } + i++; + break; + } + } + if (depth || i >= n) { + return false; + } + sig += i; + n -= i; + } if (n < PAIR_LEN || sig[0] != '(' || sig[n - SKIP_ONE] != ')' || strchr(sig, '#')) { return false; } diff --git a/src/store/store.h b/src/store/store.h index 22ca614281..fcf5805114 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -23,7 +23,7 @@ typedef struct cbm_store cbm_store_t; #define CBM_STORE_OK 0 #define CBM_STORE_ERR (-1) #define CBM_STORE_NOT_FOUND (-2) -#define CBM_INDEX_FORMAT_VERSION 2 +#define CBM_INDEX_FORMAT_VERSION 3 #define CBM_STORE_CANCELLED (-3) #define CBM_STORE_SCAN_LIMIT (-4) #define CBM_STORE_CALLBACK_ERR (-5) diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index b33dea28ad..6991c15dcd 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -151,6 +151,11 @@ static const sig_case_t k_cases[] = { "(_:T)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: Int) async {}", "function_declaration", 0, "(_:Int)async"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(body: () async -> Void) {}", + "function_declaration", 0, "(body:()async=>Void)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T) where T == () -> Void {}", "function_declaration", 0, + "Void>(_:T)"}, /* Scala: every clause flattened, by-name and repeated parameters. */ {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, "class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: " diff --git a/tests/test_index_format.c b/tests/test_index_format.c index f1110a6b1a..0b7f87cd61 100644 --- a/tests/test_index_format.c +++ b/tests/test_index_format.c @@ -280,8 +280,92 @@ TEST(index_format_version_one_rebuilds) { PASS(); } +TEST(index_format_swift_collisions_and_stale_calls_rebuild) { + const RFile files[] = { + {"Service.swift", "func pick(_ x: T) { first() }\n" + "func pick(_ x: T) { second() }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { pick(value) }\n" + "func wrong() { pick(key: 1) }\n"}, + }; + RProj lp; + cbm_store_t *s = rh_index_files(&lp, files, 1); + ASSERT_NOT_NULL(s); + cbm_store_close(s); + s = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(s); + ASSERT_EQ(cbm_store_delete_nodes_by_label(s, lp.project, "Function"), CBM_STORE_OK); + char legacy_qn[512], wrong_qn[512]; + snprintf(legacy_qn, sizeof(legacy_qn), "%s.Service.pick(_:T)", lp.project); + snprintf(wrong_qn, sizeof(wrong_qn), "%s.Service.wrong()", lp.project); + cbm_node_t legacy = {.project = lp.project, .label = "Function", .name = "pick", + .qualified_name = legacy_qn, .file_path = "Service.swift", + .start_line = 2, .end_line = 2}; + cbm_node_t wrong = {.project = lp.project, .label = "Function", .name = "wrong", + .qualified_name = wrong_qn, .file_path = "Service.swift", + .start_line = 6, .end_line = 6}; + int64_t legacy_id = cbm_store_upsert_node(s, &legacy); + int64_t wrong_id = cbm_store_upsert_node(s, &wrong); + ASSERT_GT(legacy_id, 0); + ASSERT_GT(wrong_id, 0); + cbm_edge_t stale = {.project = lp.project, .source_id = wrong_id, .target_id = legacy_id, + .type = "CALLS", .properties_json = "{\"candidates\":1}"}; + ASSERT_GT(cbm_store_insert_edge(s, &stale), 0); + ASSERT_EQ(cbm_store_set_format_version(s, 2), CBM_STORE_OK); + cbm_store_close(s); + for (int run = 0; run < 2; run++) { + char *resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + if (run == 0) { + ASSERT_NOT_NULL(strstr(g_log_buf, "format_change_reindex")); + ASSERT_NOT_NULL(strstr(resp, "\"format_migration\":true")); + } else { + ASSERT_NULL(strstr(g_log_buf, "format_change_reindex")); + ASSERT_NULL(strstr(resp, "format_migration")); + } + free(resp); + s = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(s); + cbm_node_t old = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, legacy_qn, &old), CBM_STORE_NOT_FOUND); + cbm_node_t *picks = NULL; + int count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, lp.project, "pick", &picks, &count), CBM_STORE_OK); + ASSERT_EQ(count, 2); + cbm_node_t caller = {0}, bad = {0}; + char qn[512]; + snprintf(qn, sizeof(qn), "%s.Service.caller()", lp.project); + ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, qn, &caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, wrong_qn, &bad), CBM_STORE_OK); + cbm_edge_t *edges = NULL; + int n = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, bad.id, "CALLS", &edges, &n), CBM_STORE_OK); + ASSERT_EQ(n, 0); + cbm_store_free_edges(edges, n); + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &n), CBM_STORE_OK); + ASSERT_EQ(n, 2); + for (int i = 0; i < 2; i++) { + ASSERT_TRUE(edges[i].target_id == picks[0].id || edges[i].target_id == picks[1].id); + ASSERT_NOT_NULL(strstr(edges[i].properties_json, "\"candidates\":2")); + ASSERT_EQ(picks[i].start_line, picks[i].end_line); + ASSERT_TRUE(picks[i].start_line == 1 || picks[i].start_line == 2); + } + cbm_store_free_edges(edges, n); + cbm_store_free_nodes(picks, count); + cbm_node_free_fields(&caller); + cbm_node_free_fields(&bad); + int format = -1; + ASSERT_EQ(cbm_store_get_format_version(s, &format), CBM_STORE_OK); + ASSERT_EQ(format, CBM_INDEX_FORMAT_VERSION); + cbm_store_close(s); + } + rh_cleanup(&lp, NULL); + PASS(); +} + SUITE(index_format) { RUN_TEST(index_format_siblings_distinct_and_searchable); RUN_TEST(index_format_legacy_index_rebuilds_and_repairs); RUN_TEST(index_format_version_one_rebuilds); + RUN_TEST(index_format_swift_collisions_and_stale_calls_rebuild); } diff --git a/tests/test_registry.c b/tests/test_registry.c index 21e57887d1..4683f25e91 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1329,6 +1329,16 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { } const char *out[8] = {0}; + const char *generic_qns[] = {"proj.Service.constrainedVoid>(_:T)", + "proj.Service.constrained(_:T)async"}; + for (int i = 0; i < 2; i++) { + cbm_registry_add(r, "constrained", generic_qns[i], "Method"); + cbm_registry_set_swift_signature(r, generic_qns[i], 0, 1); + } + CBMCallArg generic_arg = {0}; + CBMCall generic_call = {.callee_name = "Service.constrained", .args = &generic_arg, + .arg_count = 1}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 2); CBMCallArg args[3] = {{.keyword = "name"}}; CBMCall call = {.callee_name = "Service.work", .args = args, .arg_count = 1}; int n = cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8); From 5d214b3eb7507a79b762dbd50dfdc60570be0a69 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 13:49:07 +0800 Subject: [PATCH 29/49] docs(swift): archive history and provide portable graph and MCP repro Signed-off-by: DavidHLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 446 ++---------------- .../repro/issue2061_swift_identity/README.md | 318 ++----------- .../swift-combined-driver.c.txt => driver.c} | 4 +- .../evidence/2026-10-05/README.md | 26 - .../evidence/2026-10-05/final-graph.stdout | 19 - .../evidence/2026-10-05/final-mcp.stdout | 16 - .../evidence/2026-10-05/noble-cli-caller.json | 1 - .../evidence/2026-10-05/noble-cli-name.json | 1 - .../evidence/2026-10-05/noble-cli-search.json | 1 - .../evidence/2026-10-05/noble-cli-target.json | 1 - .../evidence/2026-10-05/noble-cli-tree.json | 1 - .../2026-10-05/objectscript-routine.stderr | 6 - .../2026-10-05/objectscript-udl.stderr | 6 - .../2026-10-05/swift-cli-validate.py.txt | 44 -- .../swift-objectscript-baseline-probe.c.txt | 24 - tests/repro/issue2061_swift_identity/repro.mk | 8 + .../repro/issue2061_swift_identity/syntax.mk | 5 - 17 files changed, 89 insertions(+), 838 deletions(-) rename tests/repro/issue2061_swift_identity/{evidence/2026-10-05/swift-combined-driver.c.txt => driver.c} (69%) delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/README.md delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-graph.stdout delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-mcp.stdout delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-caller.json delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-name.json delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-search.json delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-target.json delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-tree.json delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-routine.stderr delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-udl.stderr delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-cli-validate.py.txt delete mode 100644 tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-objectscript-baseline-probe.c.txt create mode 100644 tests/repro/issue2061_swift_identity/repro.mk delete mode 100644 tests/repro/issue2061_swift_identity/syntax.mk diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md index d6a5c93cf0..c0cd6456da 100644 --- a/docs/SWIFT_IDENTITY_VALIDATION.md +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -1,398 +1,52 @@ # Swift identity validation -This records local evidence for issue #2061 and PR #2436: Swift -callable identity and conservative overload candidates, including candidate -counts and multiple trailing closure labels. The scope remains one language, -one claim. - -## Current main integration — 2026-10-05 - -This section supersedes the readiness and dependency statements in the historical -snapshots below. Those results retain their original tested SHA; they are not -reruns of this delivery. The current implementation was tested at -`0f294df8c1fc1be3b00e948150c2f54f06708cb3` (code checkpoint C). -Any subsequent documentation-only commit preserves C's source inputs rather -than claiming that tests ran against the later Git metadata. - -Parent #2342 merged as `01fd6cb5e680662d55b040703d6b08b128b8e10f` and is -an ancestor of current main `268a9d8886642eb7f9b2ce45f5ce27cdecf0f519`. -Merge `251c06ac988fe96059581dadd5b901428e7164ce` integrated main into -`feat/issue-2061-swift-identity` without rewriting published history. PR #2436 -now targets main. Integration preserves upstream route-mount emission and adds -the missing emitter arguments to the Swift paths. No parent implementation is -duplicated in the main-relative diff. - -The additional regression fixes optional parameters preceding required -parameters with the same label, including unlabeled parameters. The former -greedy match could consume the required argument too early. Bounded cursor -transitions now retain every compatible binding. The existing deterministic -trailing-closure rules and variadic behavior remain in force. Serial and parallel -pipeline tests assert candidate edges and counts, including incompatible labels; -literal spelling is not used to guess overload types. Existing argument, -parameter, bucket and shared signature limits remain documented limitations; -the DAV-63 long-signature work is separate. - -### Executed checks - -All commands ran on **remote-dev**, not the local editing machine. Native host: -omarchy, GCC/G++ 16.2.1 (20260810), glibc 2.44, eight CPUs. Evidence root: -`/home/david/swift-delivery-20261005`. Logs, exit files, source provenance and -executables are retained there. No sanitizer suppression was added. - -| Check at C | Command / evidence prefix | Result and limit | -| --- | --- | --- | -| Focused suites | `scripts/test.sh --suites extraction,callable_sig,registry,pipeline,index_format`; `final-code-focused` | **825 passed, exit 0**; two baseline ObjectScript UBSan diagnostics, so this aggregate is not sanitizer-clean | -| Original graph reproduction | combined driver `graph .../final-graph`; `final-graph` | **10/10, exit 0**, ASan/UBSan/LSan clean; fresh production pipeline/store graph | -| MCP presentation | combined driver `mcp issue2061-swift-identity`; `final-mcp` | **5/5, exit 0**, ASan/UBSan/LSan clean; all JSON envelopes and the exact text tree observed | -| Pipeline TSan | `scripts/test.sh --tsan TEST_TSAN_SUITES=pipeline`; `final-tsan-pipeline` | **325 passed, exit 0**, including Swift serial/parallel overload, default and trailing-closure regressions; no TSan report | -| Default broad TSan | `timeout 3600 scripts/test.sh --tsan`; `final-tsan` | **exit 124**, timed out in daemon-runtime suite before pipeline; not a completed broad pass | -| Native production build | `scripts/build.sh BUILD_DIR=build/swift-production`; `final-production-build` | **exit 0**; native CLI startup fails before main in mimalloc/C++ locale initialization | -| Ubuntu production build | `scripts/build.sh BUILD_DIR=build/swift-noble-prod`; `noble-production-build2` | **exit 0**, standard production allocator/flags; source mounted read-only | -| Production CLI | index plus five queries; `noble-cli-*` | **index exit 0, five observations pass**, distinct signature display, counts, correct paths and exact text tree | -| Full canonical entry | `scripts/test.sh`; `final-code-full-test` | **exit 1 at Step0x**, same Scoop newest-release metadata contract failure on main; dynamic suites/production guards not reached | -| Full lint | `scripts/lint.sh`; intermediate `final-lint` and `baseline-lint` logs | **exit 2**, baseline clang-tidy diagnostics; final registry analysis has no diagnostics on main-relative added lines | -| CI-mode lint | `timeout 900 scripts/lint.sh --ci`; `final-code-ci-lint` | memory-core check passes; **exit 124** during cppcheck, full lint acceptance incomplete | - -The baseline attribution is deliberately bounded. Main's ObjectScript UDL and -routine scanner sources are byte-identical to C. Independent ASan/UBSan probes -at main `268a9d88` reproduce both null-buffer/zero-length deserialize diagnostics -at scanner.c lines 122 and 118, with `UBSAN_OPTIONS=halt_on_error=1`, each exit 1 -(`objectscript-udl` / `objectscript-routine`). This is a direct baseline control, -not a claim that main's entire focused suite was rerun. These unrelated vendored -scanners were not changed. - -The regression's semantic RED checkpoint is -`233f5693a4302065ab8a680ce5c507aed95cec96`: integrated build fixes and the -new test, but the old greedy matcher. `scripts/test.sh --suites registry` fails -at test_registry.c:1369 (candidate count -1 instead of 2), with 78 passes and -one failure. The assertion exits before registry cleanup, producing an LSan -report of 77,323 bytes in 108 allocations. This is a failing RED run, not a clean -sanitizer run. The earlier `b4e3daf0` attempt failed compilation and is not the -semantic RED proof. - -For the original reproduction, an external launcher only renames the two -existing drivers' main functions and graph counters, then dispatches `graph` -or `mcp`. It compiles the unchanged production sources once through: - -```bash -make -j4 -f Makefile.cbm build/c/test-runner \ - ALL_TEST_SRCS=/home/david/swift-delivery-20261005/swift-combined-driver.c -ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ -UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ -timeout 180 build/c/test-runner graph "$evidence/final-graph" -# After the graph writer closes, SQLite's backup API creates a fresh private -# cache/issue2061-swift-identity.db from the persisted graph, without overwriting. -CBM_CACHE_DIR="$evidence/mcp-cache" CBM_ALLOWED_ROOT="$evidence/final-graph/repo" \ -CBM_RUNTIME_DIR="$evidence/mcp-runtime" \ -ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ -UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ -timeout 180 build/c/test-runner mcp issue2061-swift-identity -``` - -The fresh graph has two stored bare-name `work` nodes at lines 5–7 and 10–12, -with label/type-qualified names. Only `work(flag:Bool)` calls target; the caller -reaches only `work(name:String)`; target's depth-three inbound traversal has no -false caller. MCP and CLI independently confirm signature-bearing display -leaves, exact totals and paths. Stored bare names and displayed qualified-name -leaves are separate contracts. Persistence, metadata restoration and index-format -rebuild behavior are additionally covered by the current focused suites. - -Native startup is an environment failure: a minimal no-Swift C++ program linked -with the exact production mimalloc object also crashes before main on glibc2.44. -The same object/probe enters main successfully in the existing remote Docker -image `codebase-memory-clang-tidy21:swift2061-noble-v1`, ID -`sha256:e24dbbd5c42fea34b839fc435d7110b9c52263ce00378299a956950d6184725a`. -This verified Ubuntu24.04.5/GCC13.3/glibc2.39 environment was therefore used for -the actual production CLI, with network disabled and private cache/runtime. -Git is absent inside this image, so build version metadata falls back to dev; -host Git checks establish source C. The first container build stopped before -compilation because Git was absent; only the second completed build is a pass. - -CLI commands are `cli --json index_repository --repo-path /evidence/cli-fixture ---mode fast --name issue2061-swift-identity`, `search_graph --name-pattern -'^work$' --format json --limit 20`, and `trace_path` for target inbound depth3, -onlyCallsOverloadB outbound depth1, and the fully qualified name overload -outbound depth1 (JSON, limit100, max-output-tokens3200). A fifth query requests -target's default text tree. All use project `issue2061-swift-identity`. - -| Retained artifact | SHA-256 | -| --- | --- | -| Focused runner | `f0ca3564c49e7424336c2d336c10d9faaba6e160cf320969458e78968e1722cb` | -| Focused log | `e32de50335b9016144df5ac103ed8d51a46ca2624e1c792e1a3c380565b9c83f` | -| Combined launcher | `d920968ed9f19b265e806945485c3143969ec951fe5e9744ef5612956ce36f57` | -| Combined driver executable | `24addf3107f959469b377d56732bcd81703e5c0d8b6b2d94d8cab7358c2174ad` | -| Graph stdout | `8c1cb416f731229c684d058c184f7ed86d1b7fa986c35b4e006da08a133ce661` | -| MCP stdout | `cebbf9a59dcd5de21118d56adb85a50228ca99f885c2b2522d1177d1095a3efd` | -| CLI validator | `54c640bb3f28d912822fd11e73103706984d49a9a33ba812d1a86e1dbd454bb3` | -| Ubuntu production executable | `c6aac37edc7efb39b1cd9ab4bf902675c6fa882110fc98fa8dccd6717cb5042d` | - -GitHub CI/DCO/CodeQL for C remain queued/pending at the publication checkpoint; -the earlier starting-head DCO success is historical. Required checks and -maintainer approval are not supplied by local evidence. Upstream review and -merge remain external; no branch-protection bypass or approval substitution -was attempted. DAV-61 is independently Done; DAV-62 remains future multilingual -work. The rest of this document is historical evidence. - -## Tested source - -The A-only checkpoint contains the seven Swift implementation and regression -test paths committed in `fix(swift): match multiple trailing closure labels`. -It was tested from a fresh `git archive` snapshot, without uncommitted -infrastructure changes or Git metadata. - -Historical publication snapshot, recorded before the checkpoint was pushed: -public PR head S was `b0958c6cd36c68825fac55cc58934181a2251192` and did -not contain the local A checkpoint. A's parent was local merge -`b7c86690618ed0accf137a42a987887872475fc3`; the checkpoint and this document -were then unpublished. That publication status is superseded: public harness -checkpoint `69e58c0df998e350d14fd9db8e8d205f7bf6036a` includes both. -PR readiness remains unestablished. - -| Identity | Value | -| --- | --- | -| Commit | `761ea8ca432ae0f58a6bb77c438ccedcb1ade3a1` | -| Tree | `b1718c8b0e14eb1bd7d2a2f6da1bda024e4a1912` | -| Archive SHA-256 | `d934685cc5e521060a8ddb29dcb65e5961c10d103763375e647a9e9da6a1617e` | -| Content manifest SHA-256 | `10011442026e4c5a4900c6b24ee19dda1ea3c1eda15e8bf88b0eec35b4ef77de` | - -The archive path set matched all 2,254 tracked paths, including one symlink. -The content manifest contains sorted file SHA-256 records and symlink targets. -Its before/after comparison returned **0**, with identical hashes. - -## Focused result - -From the archived source root, with `VALIDATION_BUILD_DIR` set to a separate -temporary build directory: - -```bash -scripts/test.sh --suites extraction,callable_sig,registry,pipeline \ - BUILD_DIR="$VALIDATION_BUILD_DIR" -``` - -The canonical iteration entry compiled the standard test runner and executed -exactly these four suites: - -| Suite | Relevant coverage | -| --- | --- | -| `extraction` | Multiple trailing closures, bounded labels, compact/spill round trips | -| `callable_sig` | Swift signature identity, defaults, identity length bounds | -| `registry` | Conservative overload candidates, closure labels, defaults and registration order | -| `pipeline` | Serial/parallel candidates and incremental signature restoration | - -Environment: Linux x86_64, GCC/G++ **16.2.1 20260810**, eight build cores. -The repository's default ASan+UBSan settings and the -`sanitized=1 test_seams=1` build-config assertion remained enabled. - -- Started: **2026-10-03T01:45:48Z**. -- Finished: **2026-10-03T02:12:28Z**. -- Test command exit: **0**. -- Complete runner summary: **787 passed**. -- Archived source content remained identical after execution. - -## Evidence limits and earlier failures - -At the time of the 787-pass focused run, no attributable raw Swift negative -reproduction log had been found for parent baseline -`5538355530bb126c3041f7fcf8ef7a82b4bb3fec`. That focused run alone did not -establish RED/GREEN. The subsequent original-fixture comparison below fills -the graph-layer reproduction gap without changing the focused run's scope. - -Earlier checks used combined working trees containing Swift and independent -infrastructure changes, based on -`b7c86690618ed0accf137a42a987887872475fc3`. Their recorded -`git diff --binary` SHA-256 scopes differ: - -- GCC TSan and daemon smoke: - `b1c2b8cb54ab868ab7f053f1936ecc48370f7cd3123cb5f5efd23858a114982c`. -- The 131-source analyzer run, before the parser patch: - `c0ac0734e190a79031c5a430e4cd71f52e6280bdfbd47bf9631cfcb8d397c1be`. - -These results apply to their respective snapshots and were not rerun on the -A-only archive: - -| Earlier check | Recorded result and scope | -| --- | --- | -| GCC TSan | Canonical full TSan leg failed: exit 2; 1,220 passed, 2 failed, 8 skipped. Daemon frontend EOF fixtures hit the existing 90-second child alarm. | -| Daemon smoke | Production daemon/standalone CLI smoke failed: exit 2; standalone CLI created a daemon socket, violating the smoke test's one-shot expectation. | -| Memory analysis | Canonical 131-source analysis remained FAIL. Verified execution reported findings; earlier provisional green results with missing-tool/parser failure risks were invalid. Bounded LLVM 21/22 comparisons over seven translation units found the same 16 diagnostic lines across parent/local/combined snapshots; that diagnostic comparison does not replace the 131-source gate. | - -The focused A-only result does not establish full acceptance or PR readiness. -The earlier failures remain open in their respective scopes. No broader CLI, -daemon, Cypher, MCP or Store repair is included in this Swift checkpoint. - -## Original issue #2061 graph-layer RED/GREEN (2026-10-03) - -The coordinator accepted the original three-file fixture comparison as -graph-layer P RED / A GREEN. This tests the production pipeline and persisted -CALLS graph, not the `trace_path` presentation layer or all PR gates. It is -evidence for the overall issue #2061 repair, not isolated proof of the multiple -trailing-closure patch. - -| Role | Exact commit | -| --- | --- | -| P: parent baseline | `5538355530bb126c3041f7fcf8ef7a82b4bb3fec` | -| A: Swift candidate | `cf67f49dc2d718709846adefff5bab6cf9b671d4` | -| Shared external harness | `69e58c0df998e350d14fd9db8e8d205f7bf6036a` | - -Both sides used fresh full Git archives, separate source/build/repository/DB -directories, the same external `tests/repro/issue2061_swift_identity/main.c`, -and the existing production pipeline/store APIs in FAST mode. No resolver, -P/A source or harness was modified. Neither side was rebuilt or rerun. - -Linux x86_64; GCC/G++ **16.2.1 20260810**. The standard archived Makefile's -production/extraction/grammar dependencies and `CFLAGS_TEST` were retained: -C11, `-g -O1`, `-fsanitize=address,undefined`, -`-fno-omit-frame-pointer`, standard warning/feature/API defines, and -`TEST_SEAMS=1`. Path-normalized dry-run plans were identical; the only test -source was the external harness, with no full test suite compiled or run. -The build command, with evidence locations represented by variables, was: - -```bash -make -j8 -f Makefile.cbm "$BUILD/test-runner" \ - CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$BUILD" \ - ALL_TEST_SRCS="$HARNESS/main.c" -``` - -P/A inherited the same execution environment and shared fresh `TMPDIR`. -The evidence directory and compilation temporaries used disk-backed storage -after a capacity-only preflight blocked the initial tmpfs location; that -initial attempt started no archive/build/run. Environment evidence records -only the reviewed non-secret allowlist, with other CBM variables redacted. -Each harness invocation used `timeout 180s`, -`ASAN_OPTIONS=detect_leaks=1:halt_on_error=1` and -`UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1`. - -All timestamps below are UTC on **2026-10-03**: - -| Side | Build start / end | Build exit | Run start / end | Run exit | -| --- | --- | --- | --- | --- | -| P | 05:03:18.197110 / 05:19:16.170967 | 0 | 05:27:49.563068 / 05:27:49.764641 | 1 | -| A | 05:19:16.745556 / 05:27:49.016321 | 0 | 05:46:05.496977 / 05:46:05.680162 | 0 | - -P reported **4 passed, 6 failed, errors 0, exit 1**. The source's two `work` -overloads collapsed into one node at lines 10–12. The surviving name overload -had `name -> target`, the caller had `caller -> name`, and the depth-three -inbound traversal observed the false caller (`actual=1`, expected 0). -Those positive wrong-path observations establish the original defect; -the missing flag node and unavailable (`-1`) edge checks alone do not. - -A reported **10 passed, 0 failed, errors 0, exit 0**: distinct work nodes at -lines 5–7 and 10–12; flag -> target; name has no target edge; caller -> name -and no flag edge; no false caller within target's three inbound CALLS levels. -Both complete stderr files contain normal `level=info` pipeline logs, with -no sanitizer/runtime diagnostics. - -The external scheduler accepted P's expected exit 1, then incorrectly treated -any nonempty stderr as fatal, stopping on normal info logs before A ran. -An external continuation corrected only that control flow. It first checked -that no target process, A result files or A-run directory existed, verified -the already-built binaries and recorded environment, then ran A exactly once -using the original binary/options/TMPDIR. P was not rerun. The old stopping -record and original seal remain intact. - -SHA-256 fingerprints (full values): - -| Artifact | P | A | -| --- | --- | --- | -| Full Git archive | `cc83e94190a4abeff58d1add0f9e575ebc4fa7d639bb62873800e6177ba2acc6` | `c46f2286fb49e056c24cbf907ee098b798c6c5b9099343d57d80df1588e03df2` | -| Harness binary | `99d812bec8cb5ac2e7b642ff40566a363a3d9d183cce63b05c9c26af02bd0e34` | `f625323a623bc181fcaddedbe7a196d876984ec559985496ff043e37f4a16181` | -| Source manifest before/final | `ced6a6ee75e083666b3cacd1d3829ff9d2cc09225c952c826f941f33fa319fe3` | `c388f26045627f29bc7e4707331832d14c64d40d04edf15b61d06c17c9780b0a` | - -| Shared artifact | SHA-256 | -| --- | --- | -| Harness `main.c` | `d2cdee7ba8dca1965a653803c5f0d4362aa5f9f5b65c321c9aa3268b23a100c0` | -| `Caller.swift` (both sides) | `325eb2e1034f00f9c2bc02d88123e3eb06089f39f38e8e1b0ded21a6d8314aa7` | -| `Service.swift` (both sides) | `5e18290f7bbbc29c341d5ddffb51d2c19ed00b97726e2fad0d50ebcfbcd1a529` | -| `Sink.swift` (both sides) | `a847552c25bf3a5114a565c64a9f80d8fa659f45c0dff9d1a208c700724af094` | -| Main checkout dirty/untracked content manifest before/final | `6f9882e6b094e355cf8cfdaa56748fa4826e8adafd36c9eb08ca5b8a652c1ab0` | -| Final evidence SHA-256 manifest file | `ef33eceb9ed9d3cdbb0f8910159d37e4b2c5fcf87b015fabb66ec65f9011b2de` | - -All 24 continuation checks passed. Source manifests cover archived tracked -files, modes and symlink targets and match archive contents before/final. -Archive/binary hashes stayed unchanged. The fixtures match one another and -the original three embedded issue files; all harness file hashes remained -identical. Main checkout content, status, diff, index and HEAD were unchanged. -Every previously recorded evidence file was preserved byte-for-byte. -The final continuation seal is **2026-10-03T05:46:17.826586Z**. - -Complete commands, raw stdout/stderr, actual exits, UTC records, environment -allowlist and manifests remain in the retained external evidence directory; -private absolute locations and secrets are intentionally omitted here. -This comparison does not turn the historical mixed failures into passes, -establish full/TSan/lint/memory gates on the current PR candidate, or establish -PR readiness. Remaining Swift acceptance work requires a separately approved -plan and evidence tied to one exact final candidate. - -## MCP presentation attempt — FAIL, 2026-10-03 - -Tested candidate: `0007c1d22858e1548ca392f382275595a0a2c691`; one exact-archive build/run, no reindexing. -Build: **06:26:37.439977–06:35:25.810701 UTC**, exit **0**; run: **06:35:26.198188–06:35:26.380432 UTC**, exit **1**. -Standard GCC/G++ 16.2.1 ASan+UBSan/test-seams flags and leak detection remained enabled. - -Requests 1–4 had OBS PASS: two work identities; target inbound depth3 JSON and default tree (only flag overload, hop1, total1/eq); caller outbound (only name overload, hop1, total1/eq). -All five MCP status logs were `ok`, but stdout ends at `RAW 5 {`; request 5's complete response, OBS and final SUMMARY were not flushed and its semantics remain unverified. -LeakSanitizer reported a **1024-byte direct leak**; the entire run failed, not five observations passed or a semantic RED. -Key stack: [find_nodes_generic / Store](https://github.com/DavidHLP/codebase-memory-mcp/blob/0007c1d22858e1548ca392f382275595a0a2c691/src/store/store.c#L2779) → `cbm_store_find_nodes_by_name` → [handle_trace_call_path / MCP](https://github.com/DavidHLP/codebase-memory-mcp/blob/0007c1d22858e1548ca392f382275595a0a2c691/src/mcp/mcp.c#L9291). -The complete MCP and Store source files are byte-identical to parent P `5538355530bb126c3041f7fcf8ef7a82b4bb3fec`, as confirmed by source comparison; this does not claim a new P runtime result. -This inherited generic leak is **OUT OF SCOPE**: it is not included in the Swift fix or an additional Swift acceptance gate. No repair, suppression or rerun was performed. -Original DB, backup copy, fixtures, archived source, binary and main checkout were **unchanged**; independent cache and DB copy provided isolation, not a query-layer stored-root check. -Original graph-layer P RED/A GREEN remains unchanged; the **787 focused passes belong only to core `761ea8ca432ae0f58a6bb77c438ccedcb1ade3a1`**. - -Original seal: **2026-10-03T06:35:33.549096Z**; evidence-manifest SHA-256: `0ea8f6f2de4cc10cc9b93234f557ed2aca774abcdf859982aa972691cc4bfadb`. -Raw evidence and the verbatim 69-line record are retained externally; that record's SHA-256 is `cb6278aa60003baeb4e0ccbf47ca39f72af546d389d29fa708e428b2eb7402de`. Existing evidence and seal were not overwritten. - -The [owner's issue decision](https://github.com/DeusData/codebase-memory-mcp/issues/2061#issuecomment-5836287644) requests Swift on parent PR2342, centered on helper/regression coverage: stable labels+types QNs with bare names, compatible label/default/trailing-closure selection, all compatible candidate edges with counts, and an index-format rebuild. -The [owner's PR reply](https://github.com/DeusData/codebase-memory-mcp/pull/2436#issuecomment-5936875903) queues deeper review; it is neither approval nor a routine-rebase request. -The [automated PR acknowledgement](https://github.com/DeusData/codebase-memory-mcp/pull/2436#issuecomment-5902448775) asks for CI green **or an explanation of believed pre-existing failures**; full/native macOS/strict-P matrices were contributor validation designs or commitments, not owner-specified Swift requirements. - -## Existing index_format suite — 2026-10-03 - -One authorized invocation of the standard test entry point passed **3/3**, -exit **0**, **08:48:58.454697–08:48:59.120654 UTC**. `make -q` returned **0** -and the entry-point build reported the existing runner up to date: no rebuild. -The archived source/core was `761ea8ca432ae0f58a6bb77c438ccedcb1ade3a1`; -the then-public candidate was `34a370ce2debd9ce5721ad99d8c7be67ef3ffa2b`. -All core-to-candidate changes were validation docs or standalone repro files, -not runner inputs. Makefile, test entry point and test source matched candidate -HEAD. Working-tree WIP was excluded. GCC/G++ **16.2.1 20260810**, Make **4.4.1**, -default ASan+UBSan, and `sanitized=1 test_seams=1` build metadata were retained. - -Exact invocation, with private path bindings recorded in external evidence: - -```bash -cd "$EXACT_SOURCE" -TMPDIR="$EVIDENCE/tmp" scripts/test.sh --suites index_format \ - BUILD_DIR="$EXISTING_BUILD" CC=gcc CXX=g++ TEST_SEAMS=1 -``` - -`index_format_siblings_distinct_and_searchable`, -`index_format_legacy_index_rebuilds_and_repairs`, and -`index_format_version_one_rebuilds` passed. The last test writes format **1**, -asserts full-rebuild routing and `format_migration:true`, verifies current -format **2** and preserved file count, then checks no second rebuild on an -unchanged run. This is narrow rebuild-boundary evidence, not a full-suite or -native-macOS acceptance result; the earlier 787 result keeps its original SHA. - -Before/after archive, source manifest, runner, test source, entire old build -and main checkout were unchanged. Complete stdout/stderr and real exit/UTC -records are preserved in a new independent `/home` evidence directory; hardcoded -test fixtures still used `/tmp`, whose available space was checked separately. -No sanitizer diagnostic occurred and no other suite or lint was run. - -| SHA-256 artifact | Value | -| --- | --- | -| Archive, before/after | `d934685cc5e521060a8ddb29dcb65e5961c10d103763375e647a9e9da6a1617e` | -| Source content manifest, before/after | `10011442026e4c5a4900c6b24ee19dda1ea3c1eda15e8bf88b0eec35b4ef77de` | -| Existing runner, before/after | `ad35773a1bc097cf980e67a4215baaee1b26337560696db85a908ce3ec2df11c` | -| tests/test_index_format.c, archive and checkout | `8599852fefeee42d879b38f22305cb4e3e309ead1b16bd7e6d8954a8358f43f4` | -| New suite evidence manifest | `2ebd57e301f1e5ea3049fc5e9ed5014c0991222d57662642d6f9d778c93135a7` | -| Later build-config metadata supplement manifest | `3ab556d8a8cbf66cf45c733624a6a460ed6b7de55c3ced539bcf4e6eebdf172c` | - -Allocator scope decision: retain the existing paired -`cbm_alloc(CBM_MEM_CLASS_OTHER, ...)` / `cbm_free(CBM_MEM_CLASS_OTHER, ...)` -suffix-scan scratch hunk in `cbm_registry_find_ending_with`, introduced by -`bbd0957937ee01c483c04fe5d20f23f72bcd1fc5`. It is not Swift semantics, but -reverting those two calls increases `src/pipeline/registry.c` raw sites from -the checked-in ratchet **17** to **19**, failing lint-memory-core/lint-ci. -The allocator pair and baseline counts were not changed; this scope rationale -does not claim a fresh lint run or authorize other allocator work. +Scope: issue #2061 / PR #2436, Swift callable identity and conservative CALLS +candidates only. Bare stored names remain unchanged. Generic declarations, +constraints, where requirements and function-level async distinguish identities; +they do not provide Swift compiler type inference. Whitespace/comments normalize; +generic parameter renaming, reordered constraints and inline/where semantic +equivalence are not canonicalized. Existing signature/argument limits remain +fail-closed. Other languages remain disabled. + +Format 3 rebuilds format 1/2 indexes once because generic/async identities change +persisted QNs and old CALLS edges cannot safely be updated in place. The Swift +migration regression fabricates collided nodes and an incompatible edge, verifies +source positions and candidate counts after rebuilding, then no repeat migration. +The TS/HTML/SCSS rebuild regression remains. + +## Current repair evidence + +Review checkpoint: `39d5db48a83f94641fccbb44c8cbc0233b5e2a51`, upstream base +`268a9d8886642eb7f9b2ce45f5ce27cdecf0f519`. Regression-only RED checkpoint: +`7188414d0a301574ed26a95c23159f06b0749cf7`. Repair validation is pending. +No historical pass is claimed for the repair. Final command/result records will +be added after execution. Build/test/lint run on remote-dev, not locally. + +Portable graph/MCP commands are in the +[reproduction README](../tests/repro/issue2061_swift_identity/README.md). +Run `scripts/test.sh --suites extraction,callable_sig,registry,pipeline,index_format` +for regressions, `scripts/test.sh` for the full gate, `scripts/lint.sh` for lint, +and `scripts/test.sh --tsan TEST_TSAN_SUITES=pipeline` for parallel paths. +Standalone graph/MCP drivers are opt-in, not CI gates. + +## Public historical archive + +The [complete previous validation record](https://github.com/DavidHLP/codebase-memory-mcp/blob/39d5db48a83f94641fccbb44c8cbc0233b5e2a51/docs/SWIFT_IDENTITY_VALIDATION.md) +and [actual outputs and probes](https://github.com/DavidHLP/codebase-memory-mcp/tree/39d5db48a83f94641fccbb44c8cbc0233b5e2a51/tests/repro/issue2061_swift_identity/evidence/2026-10-05) +remain public at an immutable fork commit, with actual tested SHAs, commands, +timestamps, environments, exits and failures. They are historical records, +not current operation instructions or final-repair acceptance. + +Historical `0f294df8c1fc1be3b00e948150c2f54f06708cb3` results: 825 focused +passes/exit 0 with two ObjectScript UBSan reports (not sanitizer-clean); graph +10/10 and MCP 5/5 strict sanitizer-clean; pipeline TSan 325 passes/exit 0; +broad TSan timeout 124; full test stopped at release-metadata preflight; +full lint failed, CI-mode lint timed out; native CLI startup failed, Ubuntu +production CLI passed. These separate results do not imply full acceptance. + +Historical MCP run `0007c1d22858e1548ca392f382275595a0a2c691` exited 1 +with a 1,024-byte leak after four observations; observation five and final summary +were incomplete. MCP/Store source equality with its parent alone does **not** +establish inherited runtime behaviour. Attribution remains unconfirmed without +a same-condition parent execution; neither inherited nor Swift-caused is proven. +Archived wording claiming inherited is superseded here. diff --git a/tests/repro/issue2061_swift_identity/README.md b/tests/repro/issue2061_swift_identity/README.md index 2a38fa77a2..e80dadd52b 100644 --- a/tests/repro/issue2061_swift_identity/README.md +++ b/tests/repro/issue2061_swift_identity/README.md @@ -1,296 +1,36 @@ -# Original Swift issue #2061: standalone pipeline/store proof +# Swift overload reproduction (#2061) -## Candidate MCP presentation check (executed; FAILED) +Ordinary regressions use the repository entry point: -`mcp_driver.c` is a separate, thin main using the existing MCP server API. -It is not registered in test suites or CI. Candidate -`0007c1d22858e1548ca392f382275595a0a2c691` was run once: build exit 0, -run exit 1 with a 1024-byte LeakSanitizer leak. Observations 1–4 passed; -observation 5's response/OBS and final SUMMARY were not fully saved and remain -unverified. The separate `4d99219e41c35a4dff2c1b0f211e423b913ac65f` -checkpoint was not the executed candidate. See the -[published failure record](https://github.com/DavidHLP/codebase-memory-mcp/blob/34a370ce2debd9ce5721ad99d8c7be67ef3ffa2b/docs/SWIFT_IDENTITY_VALIDATION.md#mcp-presentation-attempt--fail-2026-10-03). -The inherited generic MCP/Store leak is **OUT OF SCOPE**; original graph-layer -P RED/A GREEN is unchanged. Obtain fresh root authorization before any further -backup, build or execution; the five-observation attempt did not pass. - -Inputs are the sealed, accepted A graph from candidate -`cf67f49dc2d718709846adefff5bab6cf9b671d4` and its original three-file fixture, -produced by harness `69e58c0df998e350d14fd9db8e8d205f7bf6036a`. -Their private locations are operator inputs, never committed artifacts. -The accepted project is `issue2061-swift-identity`. The graph DB SHA-256 is -`6e8389bdcacf62cf4956556329afc74f6312b6dbe69c3a12ffb1c0579587e4ce`; -fixture hashes are in `docs/SWIFT_IDENTITY_VALIDATION.md`. - -Layout: a new ordinary evidence directory holds `source/`, `build/`, `tmp/`, -`cache/issue2061-swift-identity.db`, logs and a fixture copy. Nothing belongs -in Git. Never reuse, overwrite, delete or modify prior evidence. Record exact -source/driver SHAs, UTC, toolchain, commands, actual exits, stdout/stderr and -before/after hashes (including original DB/sidecars and fixtures). Log only -an explicit non-secret environment allowlist; other variables are redacted. - -### Read-only consistent backup, after authorization - -The accepted DB is closed and sealed; read-only lstat/hash preflight found no -`-wal`, `-shm` or `-journal`. Reconfirm those facts and absence of writers. -Use SQLite's backup API, not a lone DB-file copy. For this sealed no-sidecar -case, `mode=ro&immutable=1` prevents SQLite from writing the original or making -sidecars. Immutable mode must NOT be used if writers or a WAL are present: -stop and obtain a separately reviewed WAL-aware snapshot plan instead. - -This template is not part of driver execution. `accepted_db`, -`accepted_fixture`, and `evidence` must be supplied privately; the destination -cache must be newly created and empty. Preserve/hash the original files and -copy the three fixture files separately without overwriting any destination. - -```python -# Run only after root approval; no database access during syntax/preflight. -import os, sqlite3 -from pathlib import Path -source = Path(os.environ["accepted_db"]).resolve(strict=True) -destination = Path(os.environ["evidence"]) / "cache/issue2061-swift-identity.db" -assert not destination.exists() -assert all(not os.path.lexists(str(source) + s) - for s in ("-wal", "-shm", "-journal")) -# Operator also checks the accepted SHA-256 and that no writer is active. -fd = os.open(destination, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) -os.close(fd) # Exclusive creation also refuses existing/broken symlinks. -uri = source.as_uri() + "?mode=ro&immutable=1" -with sqlite3.connect(uri, uri=True) as src: - with sqlite3.connect(str(destination)) as dst: - src.backup(dst) -``` - -The backup is logically identical; its physical hash may differ. Capture both -hashes. Do not rewrite project roots in it. Isolation comes from the independent -`CBM_CACHE_DIR` and DB copy: embedded MCP `search_graph`/`trace_path` do not -validate the stored root against `CBM_ALLOWED_ROOT`. The server session context -may retain the verified original fixture root, which remains read-only; it is -not a query-layer isolation check. The fixture copy preserves the source bytes. - -### API and assertions - -The driver calls `cbm_mcp_server_new(NULL)`, disables background tasks, selects -the analysis profile, sets explicit session/allowed roots, and submits -`tools/call` JSON-RPC through `cbm_mcp_server_handle`. Production resolve-store -opens the existing named DB in the independent `CBM_CACHE_DIR` query-only. -There is no indexing, direct store access, graph traversal or result filtering. - -Five observations are collected: - -1. `search_graph(^work$, json)`: exactly two identities at Service lines 5-7 - and 10-12, with labels/types in their QNs. The search matches the stored bare - name; grouped presentation's `name` cell is the signature-bearing QN suffix. -2. `trace_path(target, inbound, depth=3, json)`: exactly flag overload at hop1, - total1/relation eq; neither name overload nor false caller can be present. -3. The same target request with default tree output: exact singleton direct - table, total1/relation eq, full flag QN and hop1, no additional rows/cursor. - A presentation encoding change is a mismatch to inspect, not permission to - silently relax the assertion. -4. `trace_path(onlyCallsOverloadB, outbound, depth=1, json)`: only name overload - at hop1, total1/relation eq. -5. Name overload's exact QN, outbound depth1/json: zero callees, total0/eq. - -Each request prints the original request and raw response. Envelope checks -require JSON-RPC 2.0, matching ID, no error, explicit isError=false and exactly -one text item. JSON requires structuredContent to equal the parsed text object; -tree requires structuredContent absent. All JSON rows are checked, along with -columns, totals, exact relation, and absence of truncation/continuation. -Expected summary: passed=5 failed=0 errors=0 exit=0. Exit1 means a response -mismatch; exit2 means setup/envelope failure. Neither establishes a P RED. - -### Syntax/preflight now; build/run only after another review - -Use an exact unmodified production archive, not the dirty checkout Makefile. -The existing `syntax.mk` supports this driver without modification: - -```bash -make -f "$harness/syntax.mk" issue2061-syntax \ - CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$evidence/build" \ - ALL_TEST_SRCS="$harness/mcp_driver.c" -make -n -f Makefile.cbm "$evidence/build/test-runner" \ - CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$evidence/build" \ - ALL_TEST_SRCS="$harness/mcp_driver.c" -``` - -Preflight must show only the external driver as the test/main input, standard -production/grammar dependencies, no suite execution, and no flag changes. -Syntax/preflight success is not runtime acceptance. After root reviews the -pushed candidate, archive one approved full SHA into a fresh evidence directory; -retain the same GCC/G++ 16.2.1, Make 4.4.1 and standard test flags: - -```bash -export TMPDIR="$evidence/tmp" -make -j8 -f Makefile.cbm "$evidence/build/test-runner" \ - CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$evidence/build" \ - ALL_TEST_SRCS="$harness/mcp_driver.c" -export CBM_CACHE_DIR="$evidence/cache" -export CBM_ALLOWED_ROOT="$accepted_fixture" -if ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ -UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ -timeout 180s "$evidence/build/test-runner" issue2061-swift-identity \ - > "$evidence/run.stdout" 2> "$evidence/run.stderr"; then - run_exit=0 -else - run_exit=$? -fi -printf '%s\n' "$run_exit" > "$evidence/run.exit" -# Preserve complete stderr; an info log is not a runner failure. +```sh +scripts/test.sh --suites extraction,callable_sig,registry,pipeline,index_format ``` -Stop on input/hash/SHA mismatch, unexpected sidecars/writers, nonempty existing -destination, expanded dry-run, compiler failure, timeout/signal, sanitizer -diagnostic, invalid/error response, missing rows or incomplete output. Normal -info stderr is not fatal. Preserve the checkpoint/evidence and report; do not -change production code, rerun P, or repair global baselines. This stage cannot -establish incremental multi-trailing-closure, full-suite, CI, DCO or maintainer -acceptance. - -This runner embeds only the three original Swift files from -[issue #2061](https://github.com/DeusData/codebase-memory-mcp/issues/2061). -It uses the real production pipeline in FAST mode and queries its persisted -store. It neither implements a resolver nor copies a new resolver into P. -It is deliberately absent from `ALL_TEST_SRCS`, `TEST_REPRO_SRCS` and CI. - -**Review this checkpoint on GitHub before building or running either side.** -Syntax checks do not establish RED/GREEN. This is a focused graph proof, -not a full gate or a test of the `trace_path` presentation layer. - -The runner prints every observation and a final summary. It checks two -`work` nodes, their original file/line ranges, flag → target, name ↛ target, -caller → name, caller ↛ flag, and absence of the caller in target's inbound -CALLS traversal through depth three. Node lookup uses names, files and lines, -so it works with both P's unsuffixed QNs and A's suffixed QNs. Missing -endpoints produce unavailable edge observations (`-1`), never successful -absence checks. All semantic checks are collected rather than failing early. - -Exit meanings: **0** = all observations pass; **1** = semantic mismatches -after successful indexing/store access; **2** = setup, pipeline or store API -failure. A compiler/linker failure, timeout, signal, or sanitizer diagnostic -is an infrastructure/runtime failure, not the target RED. Only an exit-1 -run with the expected collapse/false-caller observations and no runtime -diagnostics establishes the baseline reproduction. - -## Syntax only (before review) - -From each exact source archive, using the same external checkpoint files: - -```bash -make -f "$harness/syntax.mk" issue2061-syntax \ - CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$build_dir" \ - ALL_TEST_SRCS="$harness/main.c" +`main.c` checks the original three-file issue fixture through the production +pipeline/store (10 assertions). `mcp_driver.c` checks five real JSON-RPC envelopes, +signature-bearing presentation rows, counts, caller paths and the exact text tree. +These standalone drivers are **opt-in**, not automatically executed by CI. +A graph pass does not prove MCP output. + +From the repository root on a Unix host with CONTRIBUTING.md prerequisites: + +```sh +make -f tests/repro/issue2061_swift_identity/repro.mk issue2061-repro +evidence=$(mktemp -d) +build/c/issue2061-driver graph "$evidence/graph" +mkdir "$evidence/cache" +cp "$evidence/graph/graph.db" "$evidence/cache/issue2061-swift-identity.db" +CBM_CACHE_DIR="$evidence/cache" CBM_ALLOWED_ROOT="$evidence/graph/repo" \ + build/c/issue2061-driver mcp issue2061-swift-identity ``` -`syntax.mk` only includes the archive's unmodified `Makefile.cbm` and adds -one `-fsyntax-only` recipe using its standard `CFLAGS_TEST`. It builds no -objects, links no executable and runs no fixture. It does not change flags. - -## Comparable P/A build and run (after root review) - -Run in Bash from the repository root. Set `reviewed_checkpoint` to the full -SHA root actually reviewed. Use one checkpoint's harness for both sides. -All archives, binaries, logs and generated fixtures stay outside Git. -No worktree or tracked source modification is required. - -```bash -set -euo pipefail -reviewed_checkpoint=REPLACE_WITH_ROOT_REVIEWED_FULL_SHA -evidence=$(mktemp -d /tmp/dav64-issue2061-XXXXXX) -mkdir "$evidence/harness" -git archive "$reviewed_checkpoint" tests/repro/issue2061_swift_identity \ - | tar -x -C "$evidence/harness" -harness="$evidence/harness/tests/repro/issue2061_swift_identity" -P=5538355530bb126c3041f7fcf8ef7a82b4bb3fec -A=cf67f49dc2d718709846adefff5bab6cf9b671d4 -gcc --version > "$evidence/toolchain.txt" -g++ --version >> "$evidence/toolchain.txt" -uname -a > "$evidence/platform.txt" -sha256sum "$harness/main.c" "$harness/syntax.mk" > "$evidence/harness.sha256" - -for side in P A; do - sha=${!side} - mkdir "$evidence/$side-source" - git archive "$sha" > "$evidence/$side.tar" - tar -xf "$evidence/$side.tar" -C "$evidence/$side-source" - printf '%s\n' "$sha" > "$evidence/$side.commit" - sha256sum "$evidence/$side.tar" > "$evidence/$side.archive.sha256" - build_dir="$evidence/$side-build" - ( - cd "$evidence/$side-source" - make -n -f Makefile.cbm "$build_dir/test-runner" \ - CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$build_dir" \ - ALL_TEST_SRCS="$harness/main.c" - ) > "$evidence/$side.build-plan.txt" 2>&1 -done -``` - -Before compiling, inspect both plans: the only test source must be the -external `main.c`; compiler, effective flags and dependency source sets must -be comparable (normalize the P/A source/build paths). Stop and report any -unexplained difference. The standard target name `test-runner` is retained, -but `ALL_TEST_SRCS` replaces the entire test source list with this one main. -The archive's standard production/extraction/grammar dependencies and -sanitizer flags are retained. No all-tests runner or suite is compiled/run. - -Then build **only the binary target**, never `test`, `test-focused` or -`test-repro`. For each side, record UTC start/end and the actual build exit: - -```bash -for side in P A; do - build_dir="$evidence/$side-build" - date -u +%FT%TZ > "$evidence/$side.build-start" - set +e - ( - cd "$evidence/$side-source" - make -j8 -f Makefile.cbm "$build_dir/test-runner" \ - CC=gcc CXX=g++ TEST_SEAMS=1 BUILD_DIR="$build_dir" \ - ALL_TEST_SRCS="$harness/main.c" - ) > "$evidence/$side.build.stdout" 2> "$evidence/$side.build.stderr" - rc=$? - set -e - printf '%s\n' "$rc" > "$evidence/$side.build.exit" - date -u +%FT%TZ > "$evidence/$side.build-end" - if [ "$rc" -ne 0 ]; then - printf 'BUILD FAILURE on %s; not semantic RED\n' "$side" - exit "$rc" - fi - sha256sum "$build_dir/test-runner" > "$evidence/$side.binary.sha256" -done -``` - -Use the same inherited environment for both runs. Record an allowlist of -relevant variables (compiler paths, sanitizer options, `CBM_*` resource/index -settings and locale), not a raw environment dump that might contain secrets. -Leave resolver controls enabled; do not disable LSP or alter graph behavior -to force a desired result. Use fresh per-side repositories/databases and -the same working directory. The run directory must not already exist. - -```bash -for side in P A; do - date -u +%FT%TZ > "$evidence/$side.run-start" - set +e - ( - cd "$evidence" - ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \ - UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ - timeout 180s "$evidence/$side-build/test-runner" "$evidence/$side-run" - ) > "$evidence/$side.run.stdout" 2> "$evidence/$side.run.stderr" - rc=$? - set -e - printf '%s\n' "$rc" > "$evidence/$side.run.exit" - date -u +%FT%TZ > "$evidence/$side.run-end" -done -sha256sum "$harness/main.c" "$harness/syntax.mk" > "$evidence/harness-after.sha256" -cmp "$evidence/harness.sha256" "$evidence/harness-after.sha256" -for side in P A; do - sha256sum "$evidence/$side-run/repo/Sources/"*.swift \ - > "$evidence/$side.fixture.sha256" -done -``` +The graph driver creates its fresh child directory and closes its DB handles. +Copy only after its successful exit and with no active writer/WAL. Retain outputs +and actual exits. Default flags include ASan/UBSan; do not suppress leaks. +Exit 0 means observations passed, 1 an assertion mismatch, 2 a setup/API error. +Sanitizers can make a semantically successful run fail. -Compare fixture bytes between sides and with the issue, and verify archived -source manifests before/after execution. Retain raw stdout/stderr, actual -exits, timestamps, expanded build plans, toolchain/platform and fingerprints. -Inspect all observations and sanitizer output before attributing RED/GREEN. -The three-file graph has no dependency on a daemon or a new CLI architecture. -No P/A execution result is claimed by this checkpoint. +See [validation and limitations](../../../docs/SWIFT_IDENTITY_VALIDATION.md). +[Historical evidence](https://github.com/DavidHLP/codebase-memory-mcp/tree/39d5db48a83f94641fccbb44c8cbc0233b5e2a51/tests/repro/issue2061_swift_identity/evidence/2026-10-05) +remains public at an immutable commit, including failures. Old private paths, +approvals and host launchers are not current reproduction steps. diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-combined-driver.c.txt b/tests/repro/issue2061_swift_identity/driver.c similarity index 69% rename from tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-combined-driver.c.txt rename to tests/repro/issue2061_swift_identity/driver.c index 4ee4c2d984..6505a6e9ed 100644 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-combined-driver.c.txt +++ b/tests/repro/issue2061_swift_identity/driver.c @@ -2,13 +2,13 @@ #define passed graph_passed #define failed graph_failed #define errors graph_errors -#include "/home/david/Projects/codebase-memory-mcp/tests/repro/issue2061_swift_identity/main.c" +#include "main.c" #undef main #undef passed #undef failed #undef errors #define main mcp_main -#include "/home/david/Projects/codebase-memory-mcp/tests/repro/issue2061_swift_identity/mcp_driver.c" +#include "mcp_driver.c" #undef main int main(int argc, char **argv) { diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/README.md b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/README.md deleted file mode 100644 index 77c82b4abc..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Current delivery observations - -Tested production/test source: `0f294df8c1fc1be3b00e948150c2f54f06708cb3`. -These are actual remote-dev outputs, not expected-output fixtures. See -[the validation record](../../../../../docs/SWIFT_IDENTITY_VALIDATION.md#current-main-integration--2026-10-05) -for commands, environments, exit codes, baseline attribution and limitations. - -- `final-graph.stdout`: ten original reproduction observations, exit 0. -- `final-mcp.stdout`: all five JSON-RPC requests/responses and assertions, exit 0. -- `noble-cli-*.json`: five separate production CLI envelopes, validator passes. -- `objectscript-*.stderr`: strict UBSan probes against main `268a9d88`, each exit 1. - -The `.txt` source records are the exact external launchers/probes used on the -remote host. They are excluded from automatic suite discovery. To reproduce, -copy them to the original external evidence paths (dropping `.txt`) and adjust -the absolute checkout paths if needed. The combined launcher includes the two -existing production-backed drivers unchanged, using separate process runs. -The CLI validator examines the saved envelopes rather than bypassing the CLI. -The ObjectScript probe compiles only main's two unchanged scanners, with -`-std=c11 -D_DEFAULT_SOURCE -g -O1 -fsanitize=address,undefined --fno-omit-frame-pointer` and their grammar include directories. It establishes -the specific baseline defect, not full main test acceptance. - -Current graph/MCP runs use strict ASan/UBSan and leak detection and report none. -This does not erase the historical MCP failure or the two unrelated UBSan -reports in the aggregate focused test run. diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-graph.stdout b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-graph.stdout deleted file mode 100644 index 2ff4ab5e58..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-graph.stdout +++ /dev/null @@ -1,19 +0,0 @@ -EVIDENCE root=/home/david/swift-delivery-20261005/final-graph mode=fast -NODE name=work id=12 file=Sources/Service.swift lines=5-7 qn=issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) -NODE name=work id=13 file=Sources/Service.swift lines=10-12 qn=issue2061-swift-identity.Sources.Service.Service.work(name:String) -NODE name=work id=12 file=Sources/Service.swift lines=5-7 qn=issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) -NODE name=work id=13 file=Sources/Service.swift lines=10-12 qn=issue2061-swift-identity.Sources.Service.Service.work(name:String) -NODE name=target id=9 file=Sources/Sink.swift lines=2-2 qn=issue2061-swift-identity.Sources.Sink.Sink.target() -NODE name=onlyCallsOverloadB id=17 file=Sources/Caller.swift lines=5-7 qn=issue2061-swift-identity.Sources.Caller.Caller.onlyCallsOverloadB() -OBS two_work_nodes actual=2 expected=2 PASS -OBS flag_node_at_5_7 actual=1 expected=1 PASS -OBS name_node_at_10_12 actual=1 expected=1 PASS -OBS target_node actual=1 expected=1 PASS -OBS caller_node actual=1 expected=1 PASS -OBS flag_calls_target actual=1 expected=1 PASS -OBS name_does_not_call_target actual=0 expected=0 PASS -OBS caller_calls_name actual=1 expected=1 PASS -OBS caller_does_not_call_flag actual=0 expected=0 PASS -INBOUND remaining_depth=3 source=12 target=9 -OBS target_inbound_depth3_has_no_false_caller actual=0 expected=0 PASS -SUMMARY passed=10 failed=0 errors=0 exit=0 diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-mcp.stdout b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-mcp.stdout deleted file mode 100644 index 9946e157b8..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/final-mcp.stdout +++ /dev/null @@ -1,16 +0,0 @@ -REQUEST 1 {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_graph","arguments":{"project":"issue2061-swift-identity","name_pattern":"^work$","format":"json","limit":20}}} -RAW 1 {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"label\",\"lines\",\"in\",\"out\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"file\":\"Sources/Service.swift\",\"rows\":[[\"work(flag:Bool)\",\"Method\",\"5-7\",0,2],[\"work(name:String)\",\"Method\",\"10-12\",1,0]]}],\"total\":2,\"returned\":2,\"count\":2,\"has_more\":false,\"truncated\":false}"}],"structuredContent":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","label","lines","in","out"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","file":"Sources/Service.swift","rows":[["work(flag:Bool)","Method","5-7",0,2],["work(name:String)","Method","10-12",1,0]]}],"total":2,"returned":2,"count":2,"has_more":false,"truncated":false},"isError":false}} -OBS request_1 PASS -REQUEST 2 {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"trace_path","arguments":{"project":"issue2061-swift-identity","function_name":"target","direction":"inbound","depth":3,"limit":100,"max_output_tokens":3200,"format":"json"}}} -RAW 2 {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"function\":\"target\",\"direction\":\"inbound\",\"callers_total\":1,\"callers_total_relation\":\"eq\",\"callers\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"rows\":[[\"work(flag:Bool)\",1]]}]}}"}],"structuredContent":{"function":"target","direction":"inbound","callers_total":1,"callers_total_relation":"eq","callers":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","rows":[["work(flag:Bool)",1]]}]}},"isError":false}} -OBS request_2 PASS -REQUEST 3 {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"trace_path","arguments":{"project":"issue2061-swift-identity","function_name":"target","direction":"inbound","depth":3,"limit":100,"max_output_tokens":3200}}} -RAW 3 {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"function: target\ndirection: inbound\ncallers_total: 1\ncallers_total_relation: eq\ncallers: 1 (cols: qn hop)\n issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) 1\n"}],"isError":false}} -OBS request_3 PASS -REQUEST 4 {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"trace_path","arguments":{"project":"issue2061-swift-identity","function_name":"onlyCallsOverloadB","direction":"outbound","depth":1,"limit":100,"max_output_tokens":3200,"format":"json"}}} -RAW 4 {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"{\"function\":\"onlyCallsOverloadB\",\"direction\":\"outbound\",\"callees_total\":1,\"callees_total_relation\":\"eq\",\"callees\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"rows\":[[\"work(name:String)\",1]]}]}}"}],"structuredContent":{"function":"onlyCallsOverloadB","direction":"outbound","callees_total":1,"callees_total_relation":"eq","callees":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","rows":[["work(name:String)",1]]}]}},"isError":false}} -OBS request_4 PASS -REQUEST 5 {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"trace_path","arguments":{"project":"issue2061-swift-identity","function_name":"issue2061-swift-identity.Sources.Service.Service.work(name:String)","direction":"outbound","depth":1,"limit":100,"max_output_tokens":3200,"format":"json"}}} -RAW 5 {"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"{\"function\":\"issue2061-swift-identity.Sources.Service.Service.work(name:String)\",\"direction\":\"outbound\",\"callees_total\":0,\"callees_total_relation\":\"eq\",\"callees\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[]}}"}],"structuredContent":{"function":"issue2061-swift-identity.Sources.Service.Service.work(name:String)","direction":"outbound","callees_total":0,"callees_total_relation":"eq","callees":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[]}},"isError":false}} -OBS request_5 PASS -SUMMARY passed=5 failed=0 errors=0 exit=0 diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-caller.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-caller.json deleted file mode 100644 index 6234bbac65..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-caller.json +++ /dev/null @@ -1 +0,0 @@ -{"content":[{"type":"text","text":"{\"function\":\"onlyCallsOverloadB\",\"direction\":\"outbound\",\"callees_total\":1,\"callees_total_relation\":\"eq\",\"callees\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"rows\":[[\"work(name:String)\",1]]}]}}"}],"structuredContent":{"function":"onlyCallsOverloadB","direction":"outbound","callees_total":1,"callees_total_relation":"eq","callees":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","rows":[["work(name:String)",1]]}]}},"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-name.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-name.json deleted file mode 100644 index 43bf3fda5b..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-name.json +++ /dev/null @@ -1 +0,0 @@ -{"content":[{"type":"text","text":"{\"function\":\"issue2061-swift-identity.Sources.Service.Service.work(name:String)\",\"direction\":\"outbound\",\"callees_total\":0,\"callees_total_relation\":\"eq\",\"callees\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[]}}"}],"structuredContent":{"function":"issue2061-swift-identity.Sources.Service.Service.work(name:String)","direction":"outbound","callees_total":0,"callees_total_relation":"eq","callees":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[]}},"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-search.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-search.json deleted file mode 100644 index 3083b8c003..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-search.json +++ /dev/null @@ -1 +0,0 @@ -{"content":[{"type":"text","text":"{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"label\",\"lines\",\"in\",\"out\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"file\":\"Sources/Service.swift\",\"rows\":[[\"work(flag:Bool)\",\"Method\",\"5-7\",0,2],[\"work(name:String)\",\"Method\",\"10-12\",1,0]]}],\"total\":2,\"returned\":2,\"count\":2,\"has_more\":false,\"truncated\":false}"}],"structuredContent":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","label","lines","in","out"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","file":"Sources/Service.swift","rows":[["work(flag:Bool)","Method","5-7",0,2],["work(name:String)","Method","10-12",1,0]]}],"total":2,"returned":2,"count":2,"has_more":false,"truncated":false},"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-target.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-target.json deleted file mode 100644 index 290e5f4c50..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-target.json +++ /dev/null @@ -1 +0,0 @@ -{"content":[{"type":"text","text":"{\"function\":\"target\",\"direction\":\"inbound\",\"callers_total\":1,\"callers_total_relation\":\"eq\",\"callers\":{\"qn_rule\":\"qn = qn_prefix == \\\"\\\" ? name : qn_prefix + \\\".\\\" + name\",\"cols\":[\"name\",\"hop\"],\"groups\":[{\"qn_prefix\":\"issue2061-swift-identity.Sources.Service.Service\",\"rows\":[[\"work(flag:Bool)\",1]]}]}}"}],"structuredContent":{"function":"target","direction":"inbound","callers_total":1,"callers_total_relation":"eq","callers":{"qn_rule":"qn = qn_prefix == \"\" ? name : qn_prefix + \".\" + name","cols":["name","hop"],"groups":[{"qn_prefix":"issue2061-swift-identity.Sources.Service.Service","rows":[["work(flag:Bool)",1]]}]}},"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-tree.json b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-tree.json deleted file mode 100644 index 08e536bb6f..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/noble-cli-tree.json +++ /dev/null @@ -1 +0,0 @@ -{"content":[{"type":"text","text":"function: target\ndirection: inbound\ncallers_total: 1\ncallers_total_relation: eq\ncallers: 1 (cols: qn hop)\n issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) 1\n"}],"isError":false} diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-routine.stderr b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-routine.stderr deleted file mode 100644 index 8373071e13..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-routine.stderr +++ /dev/null @@ -1,6 +0,0 @@ -internal/cbm/vendored/grammars/objectscript_routine/scanner.c:118:3: runtime error: null pointer passed as argument 2, which is declared to never be null - #0 0x5ff27e837c8d in tree_sitter_objectscript_routine_external_scanner_deserialize internal/cbm/vendored/grammars/objectscript_routine/scanner.c:118 - #1 0x5ff27e81c3bf in main /home/david/swift-delivery-20261005/swift-objectscript-baseline-probe.c:20 - #2 0x74bfde427780 (/usr/lib/libc.so.6+0x27780) (BuildId: 503200d7fda94a5dc6058d7e0694e5d1dcb2e372) - #3 0x74bfde4278b8 in __libc_start_main (/usr/lib/libc.so.6+0x278b8) (BuildId: 503200d7fda94a5dc6058d7e0694e5d1dcb2e372) - #4 0x5ff27e81c214 in _start (/home/david/swift-delivery-20261005/objectscript-baseline-probe+0x2d214) (BuildId: b680da9817d0215b62060fffff7da5fb7784794c) diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-udl.stderr b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-udl.stderr deleted file mode 100644 index ab66966c06..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/objectscript-udl.stderr +++ /dev/null @@ -1,6 +0,0 @@ -internal/cbm/vendored/grammars/objectscript_udl/scanner.c:122:3: runtime error: null pointer passed as argument 2, which is declared to never be null - #0 0x628280053d52 in tree_sitter_objectscript_udl_external_scanner_deserialize internal/cbm/vendored/grammars/objectscript_udl/scanner.c:122 - #1 0x628280046364 in main /home/david/swift-delivery-20261005/swift-objectscript-baseline-probe.c:16 - #2 0x7f5316627780 (/usr/lib/libc.so.6+0x27780) (BuildId: 503200d7fda94a5dc6058d7e0694e5d1dcb2e372) - #3 0x7f53166278b8 in __libc_start_main (/usr/lib/libc.so.6+0x278b8) (BuildId: 503200d7fda94a5dc6058d7e0694e5d1dcb2e372) - #4 0x628280046214 in _start (/home/david/swift-delivery-20261005/objectscript-baseline-probe+0x2d214) (BuildId: b680da9817d0215b62060fffff7da5fb7784794c) diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-cli-validate.py.txt b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-cli-validate.py.txt deleted file mode 100644 index 248acdfcae..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-cli-validate.py.txt +++ /dev/null @@ -1,44 +0,0 @@ -import json -from pathlib import Path - -root = Path('/home/david/swift-delivery-20261005') -results = {} -for name in ('search', 'target', 'caller', 'name', 'tree'): - envelope = json.loads((root / f'noble-cli-{name}.json').read_text()) - assert envelope['isError'] is False, name - content = envelope['content'] - assert len(content) == 1 and content[0]['type'] == 'text', name - if name != 'tree': - data = json.loads(content[0]['text']) - assert envelope['structuredContent'] == data, name - results[name] = data - else: - expected = ('function: target\ndirection: inbound\ncallers_total: 1\n' - 'callers_total_relation: eq\ncallers: 1 (cols: qn hop)\n' - ' issue2061-swift-identity.Sources.Service.Service.work(flag:Bool) 1\n') - assert content[0]['text'] == expected - assert 'structuredContent' not in envelope -search = results['search'] -assert search['total'] == search['returned'] == search['count'] == 2 -assert search['has_more'] is False and search['truncated'] is False -assert search['cols'] == ['name', 'label', 'lines', 'in', 'out'] -assert len(search['groups']) == 1 -group = search['groups'][0] -assert group['qn_prefix'] == 'issue2061-swift-identity.Sources.Service.Service' -assert group['file'] == 'Sources/Service.swift' -assert sorted(group['rows']) == [['work(flag:Bool)', 'Method', '5-7', 0, 2], - ['work(name:String)', 'Method', '10-12', 1, 0]] -for name, direction, total, leaf in (('target', 'callers', 1, 'work(flag:Bool)'), - ('caller', 'callees', 1, 'work(name:String)'), - ('name', 'callees', 0, None)): - data = results[name] - assert data[f'{direction}_total'] == total - assert data[f'{direction}_total_relation'] == 'eq' - assert data[direction]['cols'] == ['name', 'hop'] - groups = data[direction]['groups'] - if total: - assert len(groups) == 1 and groups[0]['qn_prefix'] == group['qn_prefix'] - assert groups[0]['rows'] == [[leaf, 1]] - else: - assert groups == [] -print('CLI observations: 5/5 PASS (JSON envelopes, signature display, paths/counts, tree)') diff --git a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-objectscript-baseline-probe.c.txt b/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-objectscript-baseline-probe.c.txt deleted file mode 100644 index fcfbac6a89..0000000000 --- a/tests/repro/issue2061_swift_identity/evidence/2026-10-05/swift-objectscript-baseline-probe.c.txt +++ /dev/null @@ -1,24 +0,0 @@ -#include -#include - -void *tree_sitter_objectscript_udl_external_scanner_create(void); -void tree_sitter_objectscript_udl_external_scanner_deserialize(void *, const char *, unsigned); -void tree_sitter_objectscript_udl_external_scanner_destroy(void *); -void *tree_sitter_objectscript_routine_external_scanner_create(void); -void tree_sitter_objectscript_routine_external_scanner_deserialize(void *, const char *, unsigned); -void tree_sitter_objectscript_routine_external_scanner_destroy(void *); - -int main(int argc, char **argv) { - if (argc != 2) - return 2; - if (strcmp(argv[1], "udl") == 0) { - void *p = tree_sitter_objectscript_udl_external_scanner_create(); - tree_sitter_objectscript_udl_external_scanner_deserialize(p, NULL, 0); - tree_sitter_objectscript_udl_external_scanner_destroy(p); - } else { - void *p = tree_sitter_objectscript_routine_external_scanner_create(); - tree_sitter_objectscript_routine_external_scanner_deserialize(p, NULL, 0); - tree_sitter_objectscript_routine_external_scanner_destroy(p); - } - return 0; -} diff --git a/tests/repro/issue2061_swift_identity/repro.mk b/tests/repro/issue2061_swift_identity/repro.mk new file mode 100644 index 0000000000..f328fa7419 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/repro.mk @@ -0,0 +1,8 @@ +# Explicit opt-in reproduction; not part of CI or ordinary test suites. +include Makefile.cbm +SWIFT_REPRO = tests/repro/issue2061_swift_identity +.PHONY: issue2061-repro +issue2061-repro: $(BUILD_DIR)/issue2061-driver + +$(BUILD_DIR)/issue2061-driver: $(SWIFT_REPRO)/driver.c $(SWIFT_REPRO)/main.c $(SWIFT_REPRO)/mcp_driver.c $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR) + $(CC) $(CFLAGS_TEST) -Itests -o $@ $(SWIFT_REPRO)/driver.c $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(LDFLAGS_TEST) diff --git a/tests/repro/issue2061_swift_identity/syntax.mk b/tests/repro/issue2061_swift_identity/syntax.mk deleted file mode 100644 index af2ec4522e..0000000000 --- a/tests/repro/issue2061_swift_identity/syntax.mk +++ /dev/null @@ -1,5 +0,0 @@ -# Invoke from an exact archived source root; no linking or test execution. -include Makefile.cbm -.PHONY: issue2061-syntax -issue2061-syntax: - $(CC) $(CFLAGS_TEST) -fsyntax-only $(ALL_TEST_SRCS) From 5f77b4524c68199c1103a5f0d3d9a4798e6c7ab3 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 13:49:56 +0800 Subject: [PATCH 30/49] docs(swift): clarify fresh reproduction database requirements Signed-off-by: DavidHLP --- tests/repro/issue2061_swift_identity/README.md | 1 + tests/repro/issue2061_swift_identity/main.c | 2 +- tests/repro/issue2061_swift_identity/mcp_driver.c | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/repro/issue2061_swift_identity/README.md b/tests/repro/issue2061_swift_identity/README.md index e80dadd52b..20d7ab436e 100644 --- a/tests/repro/issue2061_swift_identity/README.md +++ b/tests/repro/issue2061_swift_identity/README.md @@ -19,6 +19,7 @@ make -f tests/repro/issue2061_swift_identity/repro.mk issue2061-repro evidence=$(mktemp -d) build/c/issue2061-driver graph "$evidence/graph" mkdir "$evidence/cache" +test ! -e "$evidence/graph/graph.db-wal" cp "$evidence/graph/graph.db" "$evidence/cache/issue2061-swift-identity.db" CBM_CACHE_DIR="$evidence/cache" CBM_ALLOWED_ROOT="$evidence/graph/repo" \ build/c/issue2061-driver mcp issue2061-swift-identity diff --git a/tests/repro/issue2061_swift_identity/main.c b/tests/repro/issue2061_swift_identity/main.c index 63435c3c6d..b98b6b5eca 100644 --- a/tests/repro/issue2061_swift_identity/main.c +++ b/tests/repro/issue2061_swift_identity/main.c @@ -1,6 +1,6 @@ /* Standalone original #2061 fixture; real pipeline/store, no resolver substitute. * Exit 0: all observations pass; 1: semantic mismatch; 2: setup/API error. - * Run only after review. Retain the temporary repository and DB as evidence. + * Retain the temporary repository and DB as evidence; see README.md. */ #include "pipeline/pipeline.h" #include "store/store.h" diff --git a/tests/repro/issue2061_swift_identity/mcp_driver.c b/tests/repro/issue2061_swift_identity/mcp_driver.c index b162829725..b34ffb160d 100644 --- a/tests/repro/issue2061_swift_identity/mcp_driver.c +++ b/tests/repro/issue2061_swift_identity/mcp_driver.c @@ -1,4 +1,4 @@ -/* Read the reviewed A graph COPY through production MCP only. No indexing, +/* Read the freshly reproduced graph COPY through production MCP only. No indexing, * store queries, graph traversal, resolver replacement, or fixture creation. * Exit 0: all response checks pass; 1: mismatch; 2: setup/envelope error. */ From 6222b52a0ffe019e68eb5072040fb0465a74d0b5 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 13:51:18 +0800 Subject: [PATCH 31/49] test(swift): share incompatible-call fixture across resolver paths Signed-off-by: DavidHLP --- tests/test_pipeline.c | 82 +++++++------------------------------------ 1 file changed, 12 insertions(+), 70 deletions(-) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 2189b315f8..246b895050 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8838,74 +8838,7 @@ TEST(pipeline_swift_default_before_required_candidates_issue2061) { * and pass_parallel.c). Local `fetch(id:)` cannot take a one-argument * unlabeled URL call, so the call stays unresolved and classifies as the * global API (#856). */ -TEST(pipeline_swift_incompatible_overload_unresolved_issue2061) { - char tmp[256]; - snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftincompat_XXXXXX"); - if (!cbm_mkdtemp(tmp)) { - FAIL("tmpdir"); - } - - write_temp_file(tmp, "Sources/Api.swift", - "func fetch(id: Int) -> Int {\n" - " return id\n" - "}\n" - "func get(id: Int) {}\n"); - write_temp_file(tmp, "Sources/Caller.swift", - "class Caller {\n" - " func run() -> Int {\n" - " return fetch(\"https://api.example.com/data\")\n" - " }\n" - " func wrongLabel() { get(key: 1) }\n" - "}\n"); - - char db_path[512]; - snprintf(db_path, sizeof(db_path), "%s/swiftincompat.db", tmp); - cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); - ASSERT_NOT_NULL(p); - ASSERT_EQ(cbm_pipeline_run(p), 0); - const char *project = cbm_pipeline_project_name(p); - - cbm_store_t *s = cbm_store_open_path(db_path); - ASSERT_NOT_NULL(s); - - /* The incompatible local definition IS indexed (anti-vacuous guard) ... */ - cbm_node_t *fetches = NULL; - int fetch_count = 0; - ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "fetch", &fetches, &fetch_count), - CBM_STORE_OK); - ASSERT_EQ(fetch_count, 1); - cbm_node_t *runs = NULL; - int run_count = 0; - ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "run", &runs, &run_count), CBM_STORE_OK); - ASSERT_EQ(run_count, 1); - char wrong_qn[512]; - snprintf(wrong_qn, sizeof(wrong_qn), "%s.Sources.Caller.Caller.wrongLabel()", project); - cbm_node_t wrong = {0}; - ASSERT_EQ(cbm_store_find_node_by_qn(s, project, wrong_qn, &wrong), CBM_STORE_OK); - cbm_node_t *gets = NULL; - int get_count = 0; - ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "get", &gets, &get_count), CBM_STORE_OK); - ASSERT_EQ(get_count, 1); - ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 0); - cbm_store_free_nodes(gets, get_count); - cbm_node_free_fields(&wrong); - /* ... but the call never binds to it ... */ - ASSERT_EQ(pipeline_has_calls_edge(s, runs[0].id, fetches[0].id), 0); - cbm_store_free_nodes(fetches, fetch_count); - cbm_store_free_nodes(runs, run_count); - - /* ... and the URL call still classifies as the global API. */ - ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); - - cbm_store_close(s); - cbm_pipeline_free(p); - th_rmtree(tmp); - PASS(); -} - -/* The parallel resolver must reach the same verdict for the same fixture - * (>= 50 files routes calls through resolve_file_calls). */ -TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061) { +static int swift_incompatible_overload_fixture(bool parallel) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftincompat_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { @@ -8924,7 +8857,7 @@ TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061) { " }\n" " func wrongLabel() { get(key: 1) }\n" "}\n"); - for (int i = 0; i < 52; i++) { + for (int i = 0; parallel && i < 52; i++) { char path[64], source[80]; snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); snprintf(source, sizeof(source), "func filler%d() {}\n", i); @@ -8933,7 +8866,7 @@ TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061) { char *previous_workers = getenv("CBM_WORKERS"); char *saved_workers = previous_workers ? strdup(previous_workers) : NULL; - cbm_setenv("CBM_WORKERS", "4", 1); + cbm_setenv("CBM_WORKERS", parallel ? "4" : "1", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/swiftincompat.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); @@ -8983,6 +8916,15 @@ TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061) { PASS(); } +TEST(pipeline_swift_incompatible_overload_unresolved_issue2061) { + return swift_incompatible_overload_fixture(false); +} + +/* >=50 files exercise the separate parallel resolver with identical assertions. */ +TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061) { + return swift_incompatible_overload_fixture(true); +} + /* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's * resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls * but forces the parallel resolver, since the empty-resolution fallback is a From 5d21def4a8d251996dc371c466d93b72cefe216f Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 13:51:59 +0800 Subject: [PATCH 32/49] test(swift): require persisted incremental repair path Signed-off-by: DavidHLP --- tests/test_pipeline.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 246b895050..49ea45fb09 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8567,6 +8567,10 @@ TEST(pipeline_swift_generic_async_identity_issue2061) { cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); + if (phase) { + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), + CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + } snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); From 6a8905eac56383fa28a08edc3567ca6dd4a4bfc7 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 13:57:05 +0800 Subject: [PATCH 33/49] test(swift): verify incompatible-edge assertion detects injected faults Signed-off-by: DavidHLP --- tests/test_pipeline.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 49ea45fb09..2bce134124 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8906,6 +8906,12 @@ static int swift_incompatible_overload_fixture(bool parallel) { ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "get", &gets, &get_count), CBM_STORE_OK); ASSERT_EQ(get_count, 1); ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 0); + /* Fault injection: the same checker must detect an incompatible edge, + * which would make the preceding negative assertion fail in either path. */ + cbm_edge_t injected = {.project = project, .source_id = wrong.id, + .target_id = gets[0].id, .type = "CALLS"}; + ASSERT_GT(cbm_store_insert_edge(s, &injected), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 1); cbm_store_free_nodes(gets, get_count); cbm_node_free_fields(&wrong); ASSERT_EQ(pipeline_has_calls_edge(s, runs[0].id, fetches[0].id), 0); From 9ea6058702232029b92e73601502867bfa6a4ad1 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 13:59:59 +0800 Subject: [PATCH 34/49] style(swift): format identity regression additions Signed-off-by: DavidHLP --- tests/test_callable_sig.c | 22 ++++++++++------------ tests/test_index_format.c | 36 +++++++++++++++++++++++++----------- tests/test_pipeline.c | 14 ++++++++------ tests/test_registry.c | 4 ++-- 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index 6991c15dcd..031e37660a 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -122,7 +122,8 @@ static const sig_case_t k_cases[] = { "HTTPMethod = .post, headers: [String: String]? = nil, handler: @escaping (Int) -> Void, " "xs: Int..., io: inout Swift.Int, t: (a: Int, String)) -> UploadRequest { }\n}", "function_declaration", 0, - "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(Int)=>Void," + "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(" + "Int)=>Void," "xs:Int~,io:inout Int,t:(a:Int,String))"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class Session {\n func upload(_ data: Data, with request: URLRequest) {}\n}", @@ -136,16 +137,14 @@ static const sig_case_t k_cases[] = { "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, "(l:C,r:C)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func extra() {}", "function_declaration", 0, "()"}, - {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", - "function_declaration", 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", "function_declaration", + 0, "(_:T)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", "function_declaration", 0, "(_:T)"}, - {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, - "func f(_ x: T) where T: Equatable {}", "function_declaration", 0, - "(_:T)"}, - {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, - "func f(_ x: T) where T.Element == Int {}", "function_declaration", 0, - "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) where T: Equatable {}", + "function_declaration", 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) where T.Element == Int {}", + "function_declaration", 0, "(_:T)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f < T > ( _ x : T ) where T . Element == Int { }", "function_declaration", 0, "(_:T)"}, @@ -153,9 +152,8 @@ static const sig_case_t k_cases[] = { "function_declaration", 0, "(_:Int)async"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(body: () async -> Void) {}", "function_declaration", 0, "(body:()async=>Void)"}, - {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, - "func f(_ x: T) where T == () -> Void {}", "function_declaration", 0, - "Void>(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) where T == () -> Void {}", + "function_declaration", 0, "Void>(_:T)"}, /* Scala: every clause flattened, by-name and repeated parameters. */ {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, "class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: " diff --git a/tests/test_index_format.c b/tests/test_index_format.c index 0b7f87cd61..726f09d198 100644 --- a/tests/test_index_format.c +++ b/tests/test_index_format.c @@ -298,18 +298,29 @@ TEST(index_format_swift_collisions_and_stale_calls_rebuild) { char legacy_qn[512], wrong_qn[512]; snprintf(legacy_qn, sizeof(legacy_qn), "%s.Service.pick(_:T)", lp.project); snprintf(wrong_qn, sizeof(wrong_qn), "%s.Service.wrong()", lp.project); - cbm_node_t legacy = {.project = lp.project, .label = "Function", .name = "pick", - .qualified_name = legacy_qn, .file_path = "Service.swift", - .start_line = 2, .end_line = 2}; - cbm_node_t wrong = {.project = lp.project, .label = "Function", .name = "wrong", - .qualified_name = wrong_qn, .file_path = "Service.swift", - .start_line = 6, .end_line = 6}; + cbm_node_t legacy = {.project = lp.project, + .label = "Function", + .name = "pick", + .qualified_name = legacy_qn, + .file_path = "Service.swift", + .start_line = 2, + .end_line = 2}; + cbm_node_t wrong = {.project = lp.project, + .label = "Function", + .name = "wrong", + .qualified_name = wrong_qn, + .file_path = "Service.swift", + .start_line = 6, + .end_line = 6}; int64_t legacy_id = cbm_store_upsert_node(s, &legacy); int64_t wrong_id = cbm_store_upsert_node(s, &wrong); ASSERT_GT(legacy_id, 0); ASSERT_GT(wrong_id, 0); - cbm_edge_t stale = {.project = lp.project, .source_id = wrong_id, .target_id = legacy_id, - .type = "CALLS", .properties_json = "{\"candidates\":1}"}; + cbm_edge_t stale = {.project = lp.project, + .source_id = wrong_id, + .target_id = legacy_id, + .type = "CALLS", + .properties_json = "{\"candidates\":1}"}; ASSERT_GT(cbm_store_insert_edge(s, &stale), 0); ASSERT_EQ(cbm_store_set_format_version(s, 2), CBM_STORE_OK); cbm_store_close(s); @@ -330,7 +341,8 @@ TEST(index_format_swift_collisions_and_stale_calls_rebuild) { ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, legacy_qn, &old), CBM_STORE_NOT_FOUND); cbm_node_t *picks = NULL; int count = 0; - ASSERT_EQ(cbm_store_find_nodes_by_name(s, lp.project, "pick", &picks, &count), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_nodes_by_name(s, lp.project, "pick", &picks, &count), + CBM_STORE_OK); ASSERT_EQ(count, 2); cbm_node_t caller = {0}, bad = {0}; char qn[512]; @@ -339,10 +351,12 @@ TEST(index_format_swift_collisions_and_stale_calls_rebuild) { ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, wrong_qn, &bad), CBM_STORE_OK); cbm_edge_t *edges = NULL; int n = 0; - ASSERT_EQ(cbm_store_find_edges_by_source_type(s, bad.id, "CALLS", &edges, &n), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, bad.id, "CALLS", &edges, &n), + CBM_STORE_OK); ASSERT_EQ(n, 0); cbm_store_free_edges(edges, n); - ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &n), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &n), + CBM_STORE_OK); ASSERT_EQ(n, 2); for (int i = 0; i < 2; i++) { ASSERT_TRUE(edges[i].target_id == picks[0].id || edges[i].target_id == picks[1].id); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 2bce134124..d1f0c873b7 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8522,10 +8522,12 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { /* Distinct declarations retain their own source ranges and outgoing edges; * labels/arity alone cannot choose generic constraints or sync vs async. */ TEST(pipeline_swift_generic_async_identity_issue2061) { - const char *suffixes[] = {"pick(_:T)", "pick(_:T)", - "pick(_:T)", - "pick(_:T)", "pick(_:Int)", - "pick(_:Int)async"}; + const char *suffixes[] = {"pick(_:T)", + "pick(_:T)", + "pick(_:T)", + "pick(_:T)", + "pick(_:Int)", + "pick(_:Int)async"}; for (int mode = 0; mode < 2; mode++) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_generic_XXXXXX"); @@ -8908,8 +8910,8 @@ static int swift_incompatible_overload_fixture(bool parallel) { ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 0); /* Fault injection: the same checker must detect an incompatible edge, * which would make the preceding negative assertion fail in either path. */ - cbm_edge_t injected = {.project = project, .source_id = wrong.id, - .target_id = gets[0].id, .type = "CALLS"}; + cbm_edge_t injected = { + .project = project, .source_id = wrong.id, .target_id = gets[0].id, .type = "CALLS"}; ASSERT_GT(cbm_store_insert_edge(s, &injected), 0); ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 1); cbm_store_free_nodes(gets, get_count); diff --git a/tests/test_registry.c b/tests/test_registry.c index 4683f25e91..ea9a950fde 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1336,8 +1336,8 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { cbm_registry_set_swift_signature(r, generic_qns[i], 0, 1); } CBMCallArg generic_arg = {0}; - CBMCall generic_call = {.callee_name = "Service.constrained", .args = &generic_arg, - .arg_count = 1}; + CBMCall generic_call = { + .callee_name = "Service.constrained", .args = &generic_arg, .arg_count = 1}; ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 2); CBMCallArg args[3] = {{.keyword = "name"}}; CBMCall call = {.callee_name = "Service.work", .args = args, .arg_count = 1}; From 9a183231375f3529d64fe5f7702259b76c1ed62b Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 14:23:02 +0800 Subject: [PATCH 35/49] refactor(swift): keep signature parsing within lint limits Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 18 +++++++++------- src/pipeline/registry.c | 42 ++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index b08e8f7ebc..17df633f39 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -22,6 +22,8 @@ enum { SIG_HASH_HEX = 16, /* FNV-1a 64 rendered as hex */ SIG_OPERATOR_LEN = 8, /* strlen("operator") */ SIG_CONST_LEN = 5, /* strlen("const") */ + SIG_ASYNC_LEN = 5, /* strlen("async") */ + SIG_CHAR_LEN = 1, SIG_VOLATILE_LEN = 8, /* strlen("volatile") */ SIG_ARITY_DIGITS = 16, /* "(%d)" scratch */ SIG_CAPPED_TAIL = 1 + SIG_HASH_HEX + 1, /* "#" hex ")" */ @@ -791,17 +793,17 @@ static void sig_swift_tparams(sig_ctx_t *c, TSNode node) { size_t start = c->len; if (!ts_node_is_null(params)) { sig_type_tokens(c, params); - if (c->len > start && c->buf[c->len - 1] == '>') { + if (c->len > start && c->buf[c->len - SIG_CHAR_LEN] == '>') { c->buf[--c->len] = '\0'; } } else { - sig_raw(c, "<", 1); + sig_raw_str(c, "<"); } if (!ts_node_is_null(requirements)) { - sig_raw(c, ";", 1); + sig_raw_str(c, ";"); sig_type_tokens(c, requirements); } - sig_raw(c, ">", 1); + sig_raw_str(c, ">"); for (size_t i = start; i < c->len; i++) { if (c->buf[i] == '.') { c->buf[i] = '/'; @@ -1068,7 +1070,7 @@ static void sig_render(sig_ctx_t *c, TSNode node) { uint32_t count = ts_node_child_count(node); for (uint32_t i = 0; i < count; i++) { if (sig_node_text_is(c, ts_node_child(node, i), "async")) { - sig_raw(c, "async", 5); + sig_raw_str(c, "async"); break; } } @@ -1213,8 +1215,8 @@ size_t cbm_qn_callable_base_len(const char *qn) { size_t len = strlen(qn); /* Trailing cvref (C++): const / volatile / & / && after the ')'. */ size_t t = len; - if (t >= 5 && memcmp(qn + t - 5, "async", 5) == 0) { - t -= 5; + if (t >= SIG_ASYNC_LEN && memcmp(qn + t - SIG_ASYNC_LEN, "async", SIG_ASYNC_LEN) == 0) { + t -= SIG_ASYNC_LEN; } for (;;) { if (t > 0 && qn[t - 1] == '&') { @@ -1261,7 +1263,7 @@ size_t cbm_qn_callable_base_len(const char *qn) { size_t q = p; while (q > 0) { q--; - if (qn[q] == '>' && (q == 0 || qn[q - 1] != '=')) { + if (qn[q] == '>' && (q == 0 || qn[q - SIG_CHAR_LEN] != '=')) { adepth++; } else if (qn[q] == '<') { adepth--; diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 14eb66a513..f52b2f2b90 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1621,35 +1621,39 @@ static bool swift_parameter_add(swift_parameters_t *params, const char *entry, c return true; } +/* Declaration requirements distinguish nodes, without proving applicability. */ +static size_t swift_generic_prefix_len(const char *sig, size_t n) { + unsigned depth = 0; + for (size_t i = 0; i < n; i++) { + if (sig[i] == '<') { + depth++; + } else if (sig[i] == '>' && (i == 0 || sig[i - SKIP_ONE] != '=')) { + if (depth == 0) { + return 0; + } + if (--depth == 0) { + return i + SKIP_ONE; + } + } + } + return 0; +} + /* Split only at top-level commas; preserve the closure/variadic spelling rules. */ static bool swift_parameters_read(const char *qn, const char *name, const swift_signature_t *meta, swift_parameters_t *params) { size_t base_len = cbm_qn_callable_base_len_named(qn, name); const char *sig = qn + base_len; size_t n = strlen(sig); - if (n >= 5 && memcmp(sig + n - 5, "async", 5) == 0) { - n -= 5; + const size_t async_len = sizeof("async") - SKIP_ONE; + if (n >= async_len && memcmp(sig + n - async_len, "async", async_len) == 0) { + n -= async_len; } /* Declaration generic requirements distinguish nodes, but do not prove * applicability. Read only params when selecting compatible candidates. */ if (n && sig[0] == '<') { - unsigned depth = 0; - size_t i = 0; - for (; i < n; i++) { - if (sig[i] == '<') { - depth++; - } else if (sig[i] == '>' && (i == 0 || sig[i - 1] != '=')) { - if (depth == 0) { - return false; - } - if (--depth != 0) { - continue; - } - i++; - break; - } - } - if (depth || i >= n) { + size_t i = swift_generic_prefix_len(sig, n); + if (i == 0 || i >= n) { return false; } sig += i; From ee61f3ba931c032cf7d2791464b5b10095e261b0 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 14:28:59 +0800 Subject: [PATCH 36/49] refactor(swift): remove new static analysis violations Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 31 +++++++++++++++++------------ internal/cbm/extract_calls.c | 4 ++-- internal/cbm/service_patterns.c | 3 ++- src/pipeline/lsp_surface.c | 6 ++++-- src/pipeline/pass_calls.c | 6 +++++- src/pipeline/pass_definitions.c | 3 ++- src/pipeline/pass_parallel.c | 9 +++++++-- src/pipeline/pipeline_incremental.c | 6 +++--- 8 files changed, 43 insertions(+), 25 deletions(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 17df633f39..f4e3853b5e 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -7,6 +7,7 @@ * would misread: such a suffix degrades to its hashed form. */ #include "callable_sig.h" +#include "cbm.h" #include "helpers.h" #include #include @@ -24,6 +25,7 @@ enum { SIG_CONST_LEN = 5, /* strlen("const") */ SIG_ASYNC_LEN = 5, /* strlen("async") */ SIG_CHAR_LEN = 1, + SIG_SWIFT_DEFAULT_BITS = 64, SIG_VOLATILE_LEN = 8, /* strlen("volatile") */ SIG_ARITY_DIGITS = 16, /* "(%d)" scratch */ SIG_CAPPED_TAIL = 1 + SIG_HASH_HEX + 1, /* "#" hex ")" */ @@ -811,29 +813,32 @@ static void sig_swift_tparams(sig_ctx_t *c, TSNode node) { } } +static bool sig_swift_parameter_has_default(TSNode parameter) { + uint32_t children = ts_node_child_count(parameter); + for (uint32_t i = 0; i < children; i++) { + const char *kind = ts_node_type(ts_node_child(parameter, i)); + if (strcmp(kind, "=") == 0 || strstr(kind, "default_value") != NULL) { + return true; + } + } + return false; +} + uint64_t cbm_swift_default_mask(TSNode node, const char *source, uint8_t *count) { (void)source; uint64_t defaults = 0; unsigned parameters = 0; - int last = -1; + int last = -SIG_CHAR_LEN; uint32_t children = ts_node_child_count(node); for (uint32_t i = 0; i < children; i++) { TSNode ch = ts_node_child(node, i); const char *kind = ts_node_type(ch); if (strcmp(kind, "parameter") == 0) { last = (int)parameters++; - if (last < 64) { - uint32_t pc = ts_node_child_count(ch); - for (uint32_t j = 0; j < pc; j++) { - TSNode part = ts_node_child(ch, j); - const char *pk = ts_node_type(part); - if (strcmp(pk, "=") == 0 || strstr(pk, "default_value") != NULL) { - defaults |= UINT64_C(1) << last; - break; - } - } + if (last < SIG_SWIFT_DEFAULT_BITS && sig_swift_parameter_has_default(ch)) { + defaults |= UINT64_C(1) << last; } - } else if (last >= 0 && last < 64 && strcmp(kind, "=") == 0) { + } else if (last >= 0 && last < SIG_SWIFT_DEFAULT_BITS && strcmp(kind, "=") == 0) { defaults |= UINT64_C(1) << last; } else if (last >= 0 && (strcmp(kind, ")") == 0 || strcmp(kind, "function_body") == 0)) { break; @@ -842,7 +847,7 @@ uint64_t cbm_swift_default_mask(TSNode node, const char *source, uint8_t *count) if (count) { /* ponytail: >64 parameters have no bitmask; use a dynamic mask if Swift * code with that many parameters needs overload call resolution. */ - *count = parameters > 64 ? UINT8_MAX : (uint8_t)parameters; + *count = parameters > SIG_SWIFT_DEFAULT_BITS ? UINT8_MAX : (uint8_t)parameters; } return defaults; } diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 32c9ec0a33..cb74b3901c 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -2191,8 +2191,8 @@ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) { ca->keyword = cbm_node_text(ctx->arena, label, ctx->source); if (ca->keyword) { size_t n = strlen(ca->keyword); - if (n > 0 && ca->keyword[n - 1] == ':') { - ca->keyword = cbm_arena_strndup(ctx->arena, ca->keyword, n - 1); + if (n > 0 && ca->keyword[n - SKIP_ONE] == ':') { + ca->keyword = cbm_arena_strndup(ctx->arena, ca->keyword, n - SKIP_ONE); } } } diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 75e61c485d..c28ba9435a 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -12,6 +12,7 @@ */ #include "service_patterns.h" #include "callable_sig.h" +#include "foundation/constants.h" #include "foundation/mem_core.h" #include @@ -1072,7 +1073,7 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) { char *base_qn = NULL; const char *match_qn_text = resolved_qn; if (base_len < strlen(resolved_qn)) { - base_qn = cbm_alloc(CBM_MEM_CLASS_EXTRACT, base_len + 1); + base_qn = cbm_alloc(CBM_MEM_CLASS_EXTRACT, base_len + SKIP_ONE); if (!base_qn) { return CBM_SVC_NONE; } diff --git a/src/pipeline/lsp_surface.c b/src/pipeline/lsp_surface.c index ebb7b76dff..4be2d14b85 100644 --- a/src/pipeline/lsp_surface.c +++ b/src/pipeline/lsp_surface.c @@ -17,6 +17,7 @@ * bytes is the early-cutoff key: a body edit reserializes identically. */ #include "pipeline/lsp_surface.h" +#include "foundation/constants.h" #include "pipeline/pipeline_internal.h" #include @@ -137,8 +138,9 @@ static char *surface_file_to_json(const CBMFileResult *result, const CBMLSPDef * } yyjson_mut_val *o = yyjson_mut_obj(doc); yyjson_mut_obj_add_str(doc, o, "q", d->qualified_name); - char defaults[17]; - snprintf(defaults, sizeof(defaults), "%016llx", (unsigned long long)d->swift_default_mask); + char defaults[CBM_SZ_16 + SKIP_ONE]; + (void)snprintf(defaults, sizeof(defaults), "%016llx", + (unsigned long long)d->swift_default_mask); yyjson_mut_obj_add_strcpy(doc, o, "d", defaults); yyjson_mut_obj_add_int(doc, o, "p", d->swift_param_count); yyjson_mut_arr_add_val(swift, o); diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 0ec96679d0..7d15b7f1af 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -12,6 +12,8 @@ #include "foundation/constants.h" enum { PC_RING = 4, PC_RING_MASK = 3, PC_SIG_SCAN = 15, PC_REGEX_GRP = 2 }; +static const double PC_SWIFT_SINGLE_CONF = 0.90; +static const double PC_SWIFT_AMBIGUOUS_CONF = 0.55; /* Confidence for a service-pattern HTTP/ASYNC edge emitted when registry * resolution is empty (external, unindexed client library) — see #523. */ #define PC_SVC_PATTERN_CONF 0.5 @@ -760,7 +762,9 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM if (target && target->id != source_node->id) { cbm_resolution_t selected = {.qualified_name = candidates[i], .strategy = "swift_labels", - .confidence = swift_candidates == 1 ? 0.90 : 0.55, + .confidence = swift_candidates == SKIP_ONE + ? PC_SWIFT_SINGLE_CONF + : PC_SWIFT_AMBIGUOUS_CONF, .candidate_count = swift_candidates}; emit_classified_edge(ctx, call, source_node, target, &selected, module_qn, imp_keys, imp_vals, imp_count, false, route_mount); diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 9a50829391..3db69f1cbb 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -16,6 +16,7 @@ enum { PD_RING = 4, PD_RING_MASK = 3, PD_JSON_MARGIN = 10, PD_ESC_MARGIN = 3, PD /* Fixed bytes around a serialized JSON field: ,"key":"value" / ,"key":[...] * -> comma + 2 key quotes + colon + 2 value quotes (resp. brackets). */ enum { PD_JSON_FIELD_OVERHEAD = 6 }; +enum { PD_SWIFT_PROPS_MARGIN = 80 }; #include "pipeline/pipeline.h" #include #include @@ -285,7 +286,7 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) return; } size_t pos = (size_t)n; - if (def->qn_sig_off && bufsize - pos > 80) { + if (def->qn_sig_off && bufsize - pos > PD_SWIFT_PROPS_MARGIN) { pos += (size_t)snprintf( buf + pos, bufsize - pos, ",\"swift_defaults\":\"%016llx\",\"swift_params\":%u", (unsigned long long)def->swift_default_mask, (unsigned)def->swift_param_count); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 9331e3c9d9..06b18cbc4b 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -21,6 +21,7 @@ enum { /* Fixed bytes around a serialized JSON field: ,"key":"value" / ,"key":[...] * -> comma + 2 key quotes + colon + 2 value quotes (resp. brackets). */ PP_JSON_FIELD_OVERHEAD = 6, + PP_SWIFT_PROPS_MARGIN = 80, PP_ARGS_MARGIN = 20, /* ,"line": -> comma + key (7) + colon + up to 10 digits + NUL. */ PP_LINE_MARGIN = 24, @@ -39,6 +40,8 @@ enum { #define PP_NSEC_PER_SEC 1000000000ULL #define PP_USEC_PER_MS 1000000ULL #define PP_HALF_CONF 0.5 +static const double PP_SWIFT_SINGLE_CONF = 0.90; +static const double PP_SWIFT_AMBIGUOUS_CONF = 0.55; #define PP_FIELD_HINT_CONF 0.85 enum { PP_CSHARP_M_PREFIX_LEN = 2 }; @@ -513,7 +516,7 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) return; } size_t pos = (size_t)n; - if (def->qn_sig_off && bufsize - pos > 80) { + if (def->qn_sig_off && bufsize - pos > PP_SWIFT_PROPS_MARGIN) { pos += (size_t)snprintf( buf + pos, bufsize - pos, ",\"swift_defaults\":\"%016llx\",\"swift_params\":%u", (unsigned long long)def->swift_default_mask, (unsigned)def->swift_param_count); @@ -3235,7 +3238,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB } cbm_resolution_t selected = {.qualified_name = candidates[i], .strategy = "swift_labels", - .confidence = count == 1 ? 0.90 : 0.55, + .confidence = count == SKIP_ONE + ? PP_SWIFT_SINGLE_CONF + : PP_SWIFT_AMBIGUOUS_CONF, .candidate_count = count}; emit_service_edge(ws->local_edge_buf, source_node, target, call, &selected, module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals, diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index e48536f568..171db5db5a 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1069,9 +1069,9 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { count += strlen("\"swift_params\":"); char *mask_end = NULL; char *count_end = NULL; - unsigned long long defaults = strtoull(mask, &mask_end, 16); - unsigned long params = strtoul(count, &count_end, 10); - if (mask_end == mask + 16 && *mask_end == '"' && count_end != count && + unsigned long long defaults = strtoull(mask, &mask_end, CBM_HEX_BASE); + unsigned long params = strtoul(count, &count_end, CBM_DECIMAL_BASE); + if (mask_end == mask + CBM_SZ_16 && *mask_end == '"' && count_end != count && params <= UINT8_MAX) { cbm_registry_set_swift_signature(r, node->qualified_name, (uint64_t)defaults, (uint8_t)params); From d4f10e9e6cea2453389e7bf8787b57790e34329c Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 14:36:35 +0800 Subject: [PATCH 37/49] style(swift): preserve signature constant formatting Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index f4e3853b5e..23ae06858c 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -24,11 +24,11 @@ enum { SIG_OPERATOR_LEN = 8, /* strlen("operator") */ SIG_CONST_LEN = 5, /* strlen("const") */ SIG_ASYNC_LEN = 5, /* strlen("async") */ - SIG_CHAR_LEN = 1, - SIG_SWIFT_DEFAULT_BITS = 64, SIG_VOLATILE_LEN = 8, /* strlen("volatile") */ SIG_ARITY_DIGITS = 16, /* "(%d)" scratch */ SIG_CAPPED_TAIL = 1 + SIG_HASH_HEX + 1, /* "#" hex ")" */ + SIG_CHAR_LEN = 1, + SIG_SWIFT_DEFAULT_BITS = 64, }; static const uint64_t SIG_FNV_OFFSET = 0xcbf29ce484222325ULL; /* FNV-1a 64 basis */ From 3bde72e43b523ba09629ed343b74b6343de7acd7 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 14:38:25 +0800 Subject: [PATCH 38/49] refactor(swift): preserve service matching order without assignment conditions Signed-off-by: DavidHLP --- internal/cbm/service_patterns.c | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index c28ba9435a..1f7c470622 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -1082,24 +1082,18 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) { match_qn_text = base_qn; } cbm_svc_kind_t result = CBM_SVC_NONE; - const lib_pattern_t *p; - /* Route registration checked first — prevents gin/echo from matching * as HTTP clients (both have .get/.post suffixes). */ - if ((p = match_qn(match_qn_text, route_reg_libraries))) - result = p->kind; - else if ((p = match_qn(match_qn_text, http_libraries))) - result = p->kind; - else if ((p = match_qn(match_qn_text, async_libraries))) - result = p->kind; - else if ((p = match_qn(match_qn_text, config_libraries))) - result = p->kind; - else if ((p = match_qn(match_qn_text, grpc_libraries))) - result = p->kind; - else if ((p = match_qn(match_qn_text, graphql_libraries))) - result = p->kind; - else if ((p = match_qn(match_qn_text, trpc_libraries))) - result = p->kind; + const lib_pattern_t *const tables[] = {route_reg_libraries, http_libraries, async_libraries, + config_libraries, grpc_libraries, graphql_libraries, + trpc_libraries}; + for (size_t i = 0; i < sizeof(tables) / sizeof(tables[0]); i++) { + const lib_pattern_t *p = match_qn(match_qn_text, tables[i]); + if (p) { + result = p->kind; + break; + } + } cbm_free(CBM_MEM_CLASS_EXTRACT, base_qn); if (_svc_cache) { From f2da1f464b31117c10ba5c169fad023ac9b133e6 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 14:40:41 +0800 Subject: [PATCH 39/49] style(swift): format service matching table Signed-off-by: DavidHLP --- internal/cbm/service_patterns.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 1f7c470622..eac9ac4756 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -1085,8 +1085,8 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) { /* Route registration checked first — prevents gin/echo from matching * as HTTP clients (both have .get/.post suffixes). */ const lib_pattern_t *const tables[] = {route_reg_libraries, http_libraries, async_libraries, - config_libraries, grpc_libraries, graphql_libraries, - trpc_libraries}; + config_libraries, grpc_libraries, graphql_libraries, + trpc_libraries}; for (size_t i = 0; i < sizeof(tables) / sizeof(tables[0]); i++) { const lib_pattern_t *p = match_qn(match_qn_text, tables[i]); if (p) { From 500707c5b853da75a0a4fc7f1ae3b9364c04792e Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 15:24:28 +0800 Subject: [PATCH 40/49] docs(swift): publish final repair evidence and main controls Signed-off-by: DavidHLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 60 ++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md index c0cd6456da..0c409c7e68 100644 --- a/docs/SWIFT_IDENTITY_VALIDATION.md +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -6,7 +6,7 @@ constraints, where requirements and function-level async distinguish identities; they do not provide Swift compiler type inference. Whitespace/comments normalize; generic parameter renaming, reordered constraints and inline/where semantic equivalence are not canonicalized. Existing signature/argument limits remain -fail-closed. Other languages remain disabled. +fail-closed. Callable identity modes for other languages remain disabled. Format 3 rebuilds format 1/2 indexes once because generic/async identities change persisted QNs and old CALLS edges cannot safely be updated in place. The Swift @@ -18,9 +18,61 @@ The TS/HTML/SCSS rebuild regression remains. Review checkpoint: `39d5db48a83f94641fccbb44c8cbc0233b5e2a51`, upstream base `268a9d8886642eb7f9b2ce45f5ce27cdecf0f519`. Regression-only RED checkpoint: -`7188414d0a301574ed26a95c23159f06b0749cf7`. Repair validation is pending. -No historical pass is claimed for the repair. Final command/result records will -be added after execution. Build/test/lint run on remote-dev, not locally. +`7188414d0a301574ed26a95c23159f06b0749cf7`. Tested production source: +`f2da1f464b31117c10ba5c169fad023ac9b133e6`. Subsequent evidence-only commits +do not imply these commands were rerun at their SHA. + +Environment: remote-dev, Linux x86_64, GCC 16.2.1, glibc 2.44, +Clang/clang-format/clang-tidy 22.1.8, Cppcheck 2.21.1, GNU Make 4.4.1. +Cppcheck and its dependency were extracted from signature-verified distribution +packages into a temporary directory; no system install or new suppression. +All times below are 2026-10-05 UTC. Build/test/lint ran remotely, not locally. + +| Command/path | Time | Exit/result | +| --- | --- | --- | +| `scripts/test.sh --suites extraction,callable_sig,registry,pipeline,index_format,infrascan,edge_types_probe` | 06:41:21–07:06:59 | 0; 907 passes, two ObjectScript UBSan reports; **not sanitizer-clean** | +| `scripts/test.sh --tsan TEST_TSAN_SUITES=pipeline` | 06:41:22–06:53:14 | 0; 326 passes | +| Portable graph driver, strict ASan/UBSan/LSan | 06:51:26 | 0; 10/10 semantic observations | +| Portable real JSON-RPC MCP driver, strict ASan/UBSan/LSan | 06:51:46 | 0; 5/5 envelope/tree/count observations | +| `scripts/test.sh` | 06:41:24–06:42:55 | 1; version-metadata preflight, full C suite not reached | +| `timeout 900 scripts/lint.sh` | 06:41:26–06:56:26 | 124; clang-tidy diagnostics, cppcheck unfinished | +| `timeout 900 scripts/lint.sh --ci CPPCHECK="$cppcheck -j4"` | 06:56:26–07:08:59 | 0; complete CI lint, same rules/source scope/suppressions, parallel execution only | +| `scripts/build.sh BUILD_DIR=build/dav64-swift-prod` | 06:52:31–07:08:18 | 0; clean production build isolated from test artifacts | +| Production `--version` / `--help` | 07:08:48 | 139 / 139; native startup failure | +| DCO range, memory-core, NOLINT whitelist, whitespace | 06:42:03–06:42:12 | 0; 39 nonmerge commits signed off | + +The repaired source/header/test files also pass formatter dry-run. `$cppcheck` +denotes the verified temporary executable, with its library directory in +`LD_LIBRARY_PATH`; an installed Cppcheck needs no such temporary-path setup. +Strict drivers set `ASAN_OPTIONS=detect_leaks=1:halt_on_error=1` and +`UBSAN_OPTIONS=halt_on_error=1`. Completed clang-tidy +diagnostics mapped against `git diff -U0` contain zero errors on added PR lines; +that static mapping does not turn the full lint failure into a pass. + +Public [RED output and readonly graph](https://gist.github.com/DavidHLP/af1ac103a4fea0fa956ff0229ee6d1aa) +show six declarations collapsed to two nodes at lines 5/7, merged marker +outgoing edges, and caller candidate_count=2. Failed-assertion cleanup also +reports leaks; that RED run is not a sanitizer-clean result. +[Final-source raw outputs, run SHAs/times/exits and binary hashes](https://gist.github.com/DavidHLP/2218c816c21c5c2468c6c83990ff225f/acda57ce32afb31678d43a177ad2f44d116ac4c8) +are public. Large raw files remain complete despite API preview truncation; +downloaded runtime/lint hashes were checked against their originals. + +[Same-condition main controls](https://gist.github.com/DavidHLP/a8dcbcc4042a898575f9cb1f330377fe/269b55ffde3bc9282155a4e078cb42dc9c982400) +at `268a9d8886642eb7f9b2ce45f5ce27cdecf0f519` reproduce the Scoop newest-release +pin preflight failure (exit1), CI lint900s timeout, and full clang-tidy exit2. +The clean main production build exits0; `--version`/`--help` both exit139 with +the same `mi_free → newlocale → libstdc++ locale` startup stack. This confirms +that specific native failure is present on main too; its root cause is not +established. No equivalent parent suite run establishes attribution of the +ObjectScript reports. Current macOS/Windows, Ubuntu production CLI semantic +checks and the full C test suite remain unverified. Upstream CI/review are +separate merge gates; these results are not a maintainer approval. + +Review disposition: Confirmed identity/negative-test/migration/documentation +issues are repaired; Already Addressed literal-type narrowing stays removed; +False Positive: none established; Out of Scope: other languages, compiler type +inference and unrelated baseline fixes. Full relevant source contexts were read +after the stale graph required direct-source fallback. Portable graph/MCP commands are in the [reproduction README](../tests/repro/issue2061_swift_identity/README.md). From f234417faee50e6cf48a9be9f08ccb1ce39bb539 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 16:09:38 +0800 Subject: [PATCH 41/49] test(swift): reproduce qualified and return overload collisions Signed-off-by: DavidHLP --- tests/test_callable_sig.c | 8 ++++ tests/test_pipeline.c | 79 ++++++++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index 031e37660a..dcd66f0c0c 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -154,6 +154,14 @@ static const sig_case_t k_cases[] = { "function_declaration", 0, "(body:()async=>Void)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) where T == () -> Void {}", "function_declaration", 0, "Void>(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func work(_ x: A.Item) {}", + "function_declaration", 0, "(_:A/Item)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func work(_ x: B.Item) {}", + "function_declaration", 0, "(_:B/Item)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func pick() -> Int { 1 }", + "function_declaration", 0, "()=>Int"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func pick() -> String { \"x\" }", + "function_declaration", 0, "()=>String"}, /* Scala: every clause flattened, by-name and repeated parameters. */ {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, "class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: " diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index d1f0c873b7..340b7b4cfd 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8521,30 +8521,13 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { /* Distinct declarations retain their own source ranges and outgoing edges; * labels/arity alone cannot choose generic constraints or sync vs async. */ -TEST(pipeline_swift_generic_async_identity_issue2061) { - const char *suffixes[] = {"pick(_:T)", - "pick(_:T)", - "pick(_:T)", - "pick(_:T)", - "pick(_:Int)", - "pick(_:Int)async"}; +static int swift_declaration_identity_case(const char **suffixes, int total, + const char *service_source, const char *caller_source) { for (int mode = 0; mode < 2; mode++) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_generic_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); - write_temp_file(tmp, "Sources/Service.swift", - "class Service {\n" - "func pick(_ x: T) { mark0() }\n" - "func pick(_ x: T) { mark1() }\n" - "func pick(_ x: T) where T: Hashable { mark2() }\n" - "func pick(_ x: T) where T: Comparable { mark3() }\n" - "func pick(_ x: Int) { mark4() }\n" - "func pick(_ x: Int) async { mark5() }\n" - "}\n" - "func mark0() {}\nfunc mark1() {}\nfunc mark2() {}\n" - "func mark3() {}\nfunc mark4() {}\nfunc mark5() {}\n"); - const char *caller_source = "class Caller {\nlet service = Service()\n" - "func invoke() { self.service.pick(value) }\n}\n"; + write_temp_file(tmp, "Sources/Service.swift", service_source); write_temp_file(tmp, "Sources/Caller.swift", caller_source); for (int i = 0; mode && i < 50; i++) { char path[64], source[80]; @@ -8561,9 +8544,9 @@ TEST(pipeline_swift_generic_async_identity_issue2061) { if (phase) { char path[512]; snprintf(path, sizeof(path), "%s/Sources/Caller.swift", tmp); - write_temp_file(tmp, "Sources/Caller.swift", - "class Caller {\nlet service = Service()\n" - "func invoke() { self.service.pick(value) }\n}\n// changed\n"); + char changed[1024]; + snprintf(changed, sizeof(changed), "%s// changed\n", caller_source); + write_temp_file(tmp, "Sources/Caller.swift", changed); ASSERT_EQ(pipeline_test_set_mtime(path, 2000000000, 0), 0); } cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); @@ -8580,13 +8563,13 @@ TEST(pipeline_swift_generic_async_identity_issue2061) { int count = 0; ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "pick", &overloads, &count), CBM_STORE_OK); - ASSERT_EQ(count, 6); + ASSERT_EQ(count, total); cbm_store_free_nodes(overloads, count); char qn[512]; snprintf(qn, sizeof(qn), "%s.Sources.Caller.Caller.invoke()", project); cbm_node_t caller = {0}; ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &caller), CBM_STORE_OK); - for (int i = 0; i < 6; i++) { + for (int i = 0; i < total; i++) { cbm_node_t target = {0}, marker = {0}; snprintf(qn, sizeof(qn), "%s.Sources.Service.Service.%s", project, suffixes[i]); ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &target), CBM_STORE_OK); @@ -8604,11 +8587,13 @@ TEST(pipeline_swift_generic_async_identity_issue2061) { cbm_store_free_edges(edges, n); ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &n), CBM_STORE_OK); - ASSERT_EQ(n, 6); + ASSERT_EQ(n, total); int compatible = 0; for (int e = 0; e < n; e++) { if (edges[e].target_id == target.id) { - ASSERT_NOT_NULL(strstr(edges[e].properties_json, "\"candidates\":6")); + char candidates[64]; + snprintf(candidates, sizeof(candidates), "\"candidates\":%d", total); + ASSERT_NOT_NULL(strstr(edges[e].properties_json, candidates)); compatible++; } } @@ -8632,6 +8617,45 @@ TEST(pipeline_swift_generic_async_identity_issue2061) { PASS(); } +TEST(pipeline_swift_generic_async_identity_issue2061) { + const char *suffixes[] = {"pick(_:T)", "pick(_:T)", + "pick(_:T)", + "pick(_:T)", "pick(_:Int)", "pick(_:Int)async"}; + return swift_declaration_identity_case( + suffixes, 6, + "class Service {\n" + "func pick(_ x: T) { mark0() }\n" + "func pick(_ x: T) { mark1() }\n" + "func pick(_ x: T) where T: Hashable { mark2() }\n" + "func pick(_ x: T) where T: Comparable { mark3() }\n" + "func pick(_ x: Int) { mark4() }\n" + "func pick(_ x: Int) async { mark5() }\n}\n" + "func mark0() {}\nfunc mark1() {}\nfunc mark2() {}\n" + "func mark3() {}\nfunc mark4() {}\nfunc mark5() {}\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(value) }\n}\n"); +} + +TEST(pipeline_swift_qualified_and_return_identity_issue2061) { + const char *qualified[] = {"pick(_:A/Item)", "pick(_:B/Item)"}; + ASSERT_EQ(swift_declaration_identity_case( + qualified, 2, + "class Service {\nfunc pick(_ x: A.Item) { mark0() }\n" + "func pick(_ x: B.Item) { mark1() }\n}\n" + "func mark0() {}\nfunc mark1() {}\n" + "enum A { struct Item {} }\nenum B { struct Item {} }\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(value) }\n}\n"), 0); + const char *returns[] = {"pick()=>Int", "pick()=>String"}; + return swift_declaration_identity_case( + returns, 2, + "class Service {\nfunc pick() -> Int { mark0(); return 1 }\n" + "func pick() -> String { mark1(); return \"x\" }\n}\n" + "func mark0() {}\nfunc mark1() {}\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { let _: Int = self.service.pick() }\n}\n"); +} + /* The parallel call pass must retain both type-only overload candidates. */ TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { char tmp[256]; @@ -18469,6 +18493,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_swift_overloads_keep_argument_labels_issue2061); RUN_TEST(pipeline_swift_overloads_parallel_candidates_issue2061); RUN_TEST(pipeline_swift_generic_async_identity_issue2061); + RUN_TEST(pipeline_swift_qualified_and_return_identity_issue2061); RUN_TEST(pipeline_swift_default_before_required_candidates_issue2061); RUN_TEST(pipeline_swift_incompatible_overload_unresolved_issue2061); RUN_TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061); From 598d0681f73c6c763b2fe93d565829be18049b33 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 16:14:36 +0800 Subject: [PATCH 42/49] test(swift): cover return candidates and format-three migration Signed-off-by: DavidHLP --- tests/test_callable_sig.c | 26 +++++++++++++++++++--- tests/test_index_format.c | 46 +++++++++++++++++++++++++++++++++------ tests/test_pipeline.c | 8 +++++-- tests/test_registry.c | 15 +++++++++++++ 4 files changed, 83 insertions(+), 12 deletions(-) diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index dcd66f0c0c..f10638aded 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -124,7 +124,7 @@ static const sig_case_t k_cases[] = { "function_declaration", 0, "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(" "Int)=>Void," - "xs:Int~,io:inout Int,t:(a:Int,String))"}, + "xs:Int~,io:inout Swift/Int,t:(a:Int,String))=>UploadRequest"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class Session {\n func upload(_ data: Data, with request: URLRequest) {}\n}", "function_declaration", 0, "(_:Data,with:URLRequest)"}, @@ -132,9 +132,9 @@ static const sig_case_t k_cases[] = { "init_declaration", 0, "(frame:CGRect)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class C { subscript(index i: Int) -> Int { return 0 } }", "subscript_declaration", 0, - "(index:Int)"}, + "(index:Int)=>Int"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, - "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, "(l:C,r:C)"}, + "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, "(l:C,r:C)=>C"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func extra() {}", "function_declaration", 0, "()"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", "function_declaration", @@ -162,6 +162,19 @@ static const sig_case_t k_cases[] = { "function_declaration", 0, "()=>Int"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func pick() -> String { \"x\" }", "function_declaration", 0, "()=>String"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: [A.Item], body: (B.Item) -> A.Item) async -> (A.Item, [B.Item]) {}", + "function_declaration", 0, "(_:[A/Item],body:(B/Item)=>A/Item)async=>(A/Item,[B/Item])"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f ( _ x : [ A /* comment */ . Item ], body : ( B.Item ) -> A.Item ) async " + "-> ( A.Item, [ B.Item ] ) {}", "function_declaration", 0, + "(_:[A/Item],body:(B/Item)=>A/Item)async=>(A/Item,[B/Item])"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T) where T == () -> Void {}", "function_declaration", 0, + "Void>(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f() -> (A.Item) -> B.Item {}", "function_declaration", 0, + "()=>(A/Item)=>B/Item"}, /* Scala: every clause flattened, by-name and repeated parameters. */ {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, "class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: " @@ -314,6 +327,9 @@ TEST(callable_sig_base_len_unsuffixed_is_full) { "proj.lib/(group)/page", "proj.C.+", "proj.A.unary_!", + "p.f()=>", + "p.f()=>[Int", + "p.f()=>Int)", }; for (size_t i = 0; i < sizeof(qns) / sizeof(qns[0]); i++) { ASSERT_EQ(cbm_qn_callable_base_len(qns[i]), strlen(qns[i])); @@ -337,6 +353,10 @@ TEST(callable_sig_base_len_suffixed) { {"p.S.f(const vector&,int(*)(int))const&&", "p.S.f"}, {"p.C.Work(ref int)", "p.C.Work"}, {"p.Session.upload(_:Data,to:URL)", "p.Session.upload"}, + {"p.S.pick()=>Int", "p.S.pick"}, + {"p.S.pick()=>String", "p.S.pick"}, + {"p.S.pick()async=>(A/Item)=>B/Item", "p.S.pick"}, + {"p.S.pickVoid>(_:T)=>[A/Item]", "p.S.pick"}, {"p.K.work(this:String,(Int)=>Unit)", "p.K.work"}, {"p.O.initWithFrame(_:style:)", "p.O.initWithFrame"}, {"p.C.+(l:C,r:C)", "p.C.+"}, diff --git a/tests/test_index_format.c b/tests/test_index_format.c index 726f09d198..0474e02b9d 100644 --- a/tests/test_index_format.c +++ b/tests/test_index_format.c @@ -280,14 +280,25 @@ TEST(index_format_version_one_rebuilds) { PASS(); } -TEST(index_format_swift_collisions_and_stale_calls_rebuild) { - const RFile files[] = { - {"Service.swift", "func pick(_ x: T) { first() }\n" +static int swift_collision_migration_case(int scenario) { + const char *sources[] = {"func pick(_ x: T) { first() }\n" "func pick(_ x: T) { second() }\n" "func first() {}\nfunc second() {}\n" "func caller() { pick(value) }\n" - "func wrong() { pick(key: 1) }\n"}, - }; + "func wrong() { pick(key: 1) }\n", + "func pick(_ x: A.Item) { first() }\n" + "func pick(_ x: B.Item) { second() }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { pick(value) }\n" + "func wrong() { pick(key: 1) }\n" + "enum A { struct Item {} }\nenum B { struct Item {} }\n", + "func pick() -> Int { first(); return 1 }\n" + "func pick() -> String { second(); return \"x\" }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { let _: Int = pick() }\n" + "func wrong() { pick(key: 1) }\n"}; + const char *legacy_suffix[] = {"pick(_:T)", "pick(_:Item)", "pick()"}; + const RFile files[] = {{"Service.swift", sources[scenario]}}; RProj lp; cbm_store_t *s = rh_index_files(&lp, files, 1); ASSERT_NOT_NULL(s); @@ -296,7 +307,7 @@ TEST(index_format_swift_collisions_and_stale_calls_rebuild) { ASSERT_NOT_NULL(s); ASSERT_EQ(cbm_store_delete_nodes_by_label(s, lp.project, "Function"), CBM_STORE_OK); char legacy_qn[512], wrong_qn[512]; - snprintf(legacy_qn, sizeof(legacy_qn), "%s.Service.pick(_:T)", lp.project); + snprintf(legacy_qn, sizeof(legacy_qn), "%s.Service.%s", lp.project, legacy_suffix[scenario]); snprintf(wrong_qn, sizeof(wrong_qn), "%s.Service.wrong()", lp.project); cbm_node_t legacy = {.project = lp.project, .label = "Function", @@ -322,7 +333,7 @@ TEST(index_format_swift_collisions_and_stale_calls_rebuild) { .type = "CALLS", .properties_json = "{\"candidates\":1}"}; ASSERT_GT(cbm_store_insert_edge(s, &stale), 0); - ASSERT_EQ(cbm_store_set_format_version(s, 2), CBM_STORE_OK); + ASSERT_EQ(cbm_store_set_format_version(s, scenario ? 3 : 2), CBM_STORE_OK); cbm_store_close(s); for (int run = 0; run < 2; run++) { char *resp = index_capture(&lp); @@ -355,6 +366,20 @@ TEST(index_format_swift_collisions_and_stale_calls_rebuild) { CBM_STORE_OK); ASSERT_EQ(n, 0); cbm_store_free_edges(edges, n); + ASSERT_NEQ(picks[0].id, picks[1].id); + ASSERT_NEQ(picks[0].start_line, picks[1].start_line); + for (int i = 0; i < count; i++) { + cbm_node_t marker = {0}; + snprintf(qn, sizeof(qn), "%s.Service.%s()", lp.project, + picks[i].start_line == 1 ? "first" : "second"); + ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, qn, &marker), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, picks[i].id, "CALLS", &edges, &n), + CBM_STORE_OK); + ASSERT_EQ(n, 1); + ASSERT_EQ(edges[0].target_id, marker.id); + cbm_store_free_edges(edges, n); + cbm_node_free_fields(&marker); + } ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &n), CBM_STORE_OK); ASSERT_EQ(n, 2); @@ -377,6 +402,13 @@ TEST(index_format_swift_collisions_and_stale_calls_rebuild) { PASS(); } +TEST(index_format_swift_collisions_and_stale_calls_rebuild) { + for (int scenario = 0; scenario < 3; scenario++) { + ASSERT_EQ(swift_collision_migration_case(scenario), 0); + } + PASS(); +} + SUITE(index_format) { RUN_TEST(index_format_siblings_distinct_and_searchable); RUN_TEST(index_format_legacy_index_rebuilds_and_repairs); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 340b7b4cfd..bc7ba1a11c 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8638,14 +8638,17 @@ TEST(pipeline_swift_generic_async_identity_issue2061) { TEST(pipeline_swift_qualified_and_return_identity_issue2061) { const char *qualified[] = {"pick(_:A/Item)", "pick(_:B/Item)"}; - ASSERT_EQ(swift_declaration_identity_case( + return swift_declaration_identity_case( qualified, 2, "class Service {\nfunc pick(_ x: A.Item) { mark0() }\n" "func pick(_ x: B.Item) { mark1() }\n}\n" "func mark0() {}\nfunc mark1() {}\n" "enum A { struct Item {} }\nenum B { struct Item {} }\n", "class Caller {\nlet service = Service()\n" - "func invoke() { self.service.pick(value) }\n}\n"), 0); + "func invoke() { self.service.pick(value) }\n}\n"); +} + +TEST(pipeline_swift_return_identity_issue2061) { const char *returns[] = {"pick()=>Int", "pick()=>String"}; return swift_declaration_identity_case( returns, 2, @@ -18494,6 +18497,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_swift_overloads_parallel_candidates_issue2061); RUN_TEST(pipeline_swift_generic_async_identity_issue2061); RUN_TEST(pipeline_swift_qualified_and_return_identity_issue2061); + RUN_TEST(pipeline_swift_return_identity_issue2061); RUN_TEST(pipeline_swift_default_before_required_candidates_issue2061); RUN_TEST(pipeline_swift_incompatible_overload_unresolved_issue2061); RUN_TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061); diff --git a/tests/test_registry.c b/tests/test_registry.c index ea9a950fde..fdc951ad0d 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1339,6 +1339,21 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { CBMCall generic_call = { .callee_name = "Service.constrained", .args = &generic_arg, .arg_count = 1}; ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 2); + const char *returns[] = {"proj.Service.pick()=>Int", "proj.Service.pick()async=>(A/Item)=>B/Item"}; + for (int i = 0; i < 2; i++) { + cbm_registry_add(r, "pick", returns[i], "Method"); + cbm_registry_set_swift_signature(r, returns[i], 0, 0); + } + CBMCall returned = {.callee_name = "Service.pick"}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &returned, "proj.Caller", NULL, 0, out, 8), 2); + const char *malformed[] = {"proj.Service.bad()=>", "proj.Service.bad()=>[Int", + "proj.Service.bad()=>Int)"}; + for (int i = 0; i < 3; i++) { + cbm_registry_add(r, "bad", malformed[i], "Method"); + cbm_registry_set_swift_signature(r, malformed[i], 0, 0); + } + returned.callee_name = "Service.bad"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &returned, "proj.Caller", NULL, 0, out, 8), -1); CBMCallArg args[3] = {{.keyword = "name"}}; CBMCall call = {.callee_name = "Service.work", .args = args, .arg_count = 1}; int n = cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8); From 3113c6a7bec1832d9510b74de75d045a748eb19d Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 16:19:47 +0800 Subject: [PATCH 43/49] test(swift): guard returned operator and capped identities Signed-off-by: DavidHLP --- tests/test_callable_sig.c | 29 ++++++++++++++++++++++++++--- tests/test_registry.c | 13 ++++++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index f10638aded..509e3191a5 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -169,9 +169,6 @@ static const sig_case_t k_cases[] = { "func f ( _ x : [ A /* comment */ . Item ], body : ( B.Item ) -> A.Item ) async " "-> ( A.Item, [ B.Item ] ) {}", "function_declaration", 0, "(_:[A/Item],body:(B/Item)=>A/Item)async=>(A/Item,[B/Item])"}, - {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, - "func f(_ x: T) where T == () -> Void {}", "function_declaration", 0, - "Void>(_:T)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f() -> (A.Item) -> B.Item {}", "function_declaration", 0, "()=>(A/Item)=>B/Item"}, @@ -258,6 +255,29 @@ TEST(callable_sig_cap_keeps_identity) { char *qn = cbm_arena_sprintf(&a, "p.A.f%s", sa); ASSERT_EQ(cbm_qn_callable_base_len(qn), strlen("p.A.f")); cbm_arena_destroy(&a); + n = (size_t)snprintf(src_a, sizeof(src_a), "func f() -> ("); + for (int i = 0; i < 24; i++) { + n += (size_t)snprintf(src_a + n, sizeof(src_a) - n, "%sA.VeryLongType%d", + i ? "," : "", i); + } + memcpy(src_b, src_a, n); + snprintf(src_a + n, sizeof(src_a) - n, ",A.Last) {}"); + snprintf(src_b + n, sizeof(src_b) - n, ",B.Last) {}"); + cbm_arena_init(&a); + sa = sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_a, + "function_declaration", 0); + sa2 = sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_a, + "function_declaration", 0); + sb = sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_b, + "function_declaration", 0); + ASSERT_NOT_NULL(sa); + ASSERT_NOT_NULL(sb); + ASSERT_TRUE(strlen(sa) <= CBM_CALLABLE_SIG_MAX); + ASSERT_STR_EQ(sa, sa2); + ASSERT_STR_NEQ(sa, sb); + qn = cbm_arena_sprintf(&a, "p.A.f%s", sa); + ASSERT_EQ(cbm_qn_callable_base_len(qn), strlen("p.A.f")); + cbm_arena_destroy(&a); PASS(); } @@ -355,6 +375,9 @@ TEST(callable_sig_base_len_suffixed) { {"p.Session.upload(_:Data,to:URL)", "p.Session.upload"}, {"p.S.pick()=>Int", "p.S.pick"}, {"p.S.pick()=>String", "p.S.pick"}, + {"p.S.<(_:A,_:A)=>Bool", "p.S.<"}, + {"p.S.>(_:A,_:A)=>Bool", "p.S.>"}, + {"p.S.<(_:T,_:T)=>Bool", "p.S.<"}, {"p.S.pick()async=>(A/Item)=>B/Item", "p.S.pick"}, {"p.S.pickVoid>(_:T)=>[A/Item]", "p.S.pick"}, {"p.K.work(this:String,(Int)=>Unit)", "p.K.work"}, diff --git a/tests/test_registry.c b/tests/test_registry.c index fdc951ad0d..a559f10022 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1353,7 +1353,18 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { cbm_registry_set_swift_signature(r, malformed[i], 0, 0); } returned.callee_name = "Service.bad"; - ASSERT_EQ(cbm_registry_swift_candidates(r, &returned, "proj.Caller", NULL, 0, out, 8), -1); + ASSERT_EQ(cbm_registry_swift_candidates(r, &returned, "proj.Caller", NULL, 0, out, 8), 0); + const char *operators[] = {"<", ">"}; + for (int i = 0; i < 2; i++) { + char qn[128]; + snprintf(qn, sizeof(qn), "proj.Service.%s(_:A,_:A)=>Bool", operators[i]); + cbm_registry_add(r, operators[i], qn, "Method"); + cbm_registry_set_swift_signature(r, qn, 0, 2); + CBMCallArg operands[2] = {{0}}; + CBMCall operation = {.callee_name = operators[i], .args = operands, .arg_count = 2}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &operation, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qn); + } CBMCallArg args[3] = {{.keyword = "name"}}; CBMCall call = {.callee_name = "Service.work", .args = args, .arg_count = 1}; int n = cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8); From e3cd15a9c462cb294eafcc9ea39cbef90fafe22f Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 16:24:38 +0800 Subject: [PATCH 44/49] test(swift): preserve nested generic requirement boundaries Signed-off-by: DavidHLP --- tests/test_callable_sig.c | 6 ++++++ tests/test_registry.c | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index 509e3191a5..a4cc8bcb52 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -172,6 +172,12 @@ static const sig_case_t k_cases[] = { {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f() -> (A.Item) -> B.Item {}", "function_declaration", 0, "()=>(A/Item)=>B/Item"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T, _ y: U) -> Int where T == Box<[Int]>, U == () -> Void {}", + "function_declaration", 0, ",U==()=>Void>(_:T,_:U)=>Int"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T, _ y: U) -> Int where T == Box<() -> Void>, U == () -> Void {}", + "function_declaration", 0, "Void>,U==()=>Void>(_:T,_:U)=>Int"}, /* Scala: every clause flattened, by-name and repeated parameters. */ {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, "class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: " diff --git a/tests/test_registry.c b/tests/test_registry.c index a559f10022..5e0a5f10ce 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1339,6 +1339,12 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { CBMCall generic_call = { .callee_name = "Service.constrained", .args = &generic_arg, .arg_count = 1}; ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 2); + const char *nested = "proj.Service.nested,U==()=>Void>(_:T)=>Int"; + cbm_registry_add(r, "nested", nested, "Method"); + cbm_registry_set_swift_signature(r, nested, 0, 1); + generic_call.callee_name = "Service.nested"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], nested); const char *returns[] = {"proj.Service.pick()=>Int", "proj.Service.pick()async=>(A/Item)=>B/Item"}; for (int i = 0; i < 2; i++) { cbm_registry_add(r, "pick", returns[i], "Method"); From ab0dce1f52418ac2fe5ef2d3b16a36fdf98c1ab2 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 16:26:14 +0800 Subject: [PATCH 45/49] fix(swift): retain qualified parameter and return identities Signed-off-by: DavidHLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 20 ++++++-- internal/cbm/callable_sig.c | 83 ++++++++++++++++++++++++++++--- internal/cbm/callable_sig.h | 13 +++-- src/pipeline/registry.c | 2 +- src/store/store.h | 2 +- 5 files changed, 104 insertions(+), 16 deletions(-) diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md index 0c409c7e68..fa6117ef41 100644 --- a/docs/SWIFT_IDENTITY_VALIDATION.md +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -2,19 +2,33 @@ Scope: issue #2061 / PR #2436, Swift callable identity and conservative CALLS candidates only. Bare stored names remain unchanged. Generic declarations, -constraints, where requirements and function-level async distinguish identities; +constraints, where requirements, qualified parameter paths, explicit return types +and function-level async distinguish identities; they do not provide Swift compiler type inference. Whitespace/comments normalize; generic parameter renaming, reordered constraints and inline/where semantic equivalence are not canonicalized. Existing signature/argument limits remain fail-closed. Callable identity modes for other languages remain disabled. -Format 3 rebuilds format 1/2 indexes once because generic/async identities change +Swift keeps qualified types as `A/Item` (rather than collapsing to `Item`) and +spells explicit returns as `pick()=>Int` or `pick()async=>String`. This records +declaration syntax; it does not resolve aliases, infer implicit returns or claim +to distinguish every legal overload. Absent returns keep the existing suffix. + +Format 4 rebuilds format 1/2/3 indexes once because these identities change persisted QNs and old CALLS edges cannot safely be updated in place. The Swift migration regression fabricates collided nodes and an incompatible edge, verifies source positions and candidate counts after rebuilding, then no repeat migration. The TS/HTML/SCSS rebuild regression remains. -## Current repair evidence +## P2 repair evidence + +Review checkpoint: `500707c5b853da75a0a4fc7f1ae3b9364c04792e`. The previous +generic/where/async, negative-target, migration and documentation findings remain +resolved. New qualified-type and return-only collisions require independent +RED/GREEN and final-source verification; the historical passes below do not +establish acceptance of this repair. + +## Previous repair evidence (historical) Review checkpoint: `39d5db48a83f94641fccbb44c8cbc0233b5e2a51`, upstream base `268a9d8886642eb7f9b2ce45f5ce27cdecf0f519`. Regression-only RED checkpoint: diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 23ae06858c..caa774f6ee 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -24,6 +24,7 @@ enum { SIG_OPERATOR_LEN = 8, /* strlen("operator") */ SIG_CONST_LEN = 5, /* strlen("const") */ SIG_ASYNC_LEN = 5, /* strlen("async") */ + SIG_ARROW_LEN = 2, /* strlen("=>") */ SIG_VOLATILE_LEN = 8, /* strlen("volatile") */ SIG_ARITY_DIGITS = 16, /* "(%d)" scratch */ SIG_CAPPED_TAIL = 1 + SIG_HASH_HEX + 1, /* "#" hex ")" */ @@ -933,10 +934,11 @@ static void sig_generic_params(sig_ctx_t *c, TSNode node) { /* ── Spelling pass ─────────────────────────────────────────────── */ -/* Qualified type paths keep their last segment; "->" becomes "=>"; any other +/* Swift qualified paths use '/'; other type paths keep their last segment. + * "->" becomes "=>"; any other * '.' or "::" (a receiver function type, a leading global scope) is dropped, * so the suffix contract holds whatever the grammar produced. */ -static size_t sig_spell(char *s, size_t n) { +static size_t sig_spell(char *s, size_t n, CBMLanguage lang) { size_t out = 0; size_t i = 0; while (i < n) { @@ -947,6 +949,11 @@ static size_t sig_spell(char *s, size_t n) { sep = 2; } if (sep) { + if (lang == CBM_LANG_SWIFT) { + s[out++] = '/'; + i += sep; + continue; + } bool ident_before = out > 0 && sig_ident_char((unsigned char)s[out - 1]); bool ident_after = i + sep < n && sig_ident_char((unsigned char)s[i + sep]); if (ident_before && ident_after) { @@ -1079,6 +1086,11 @@ static void sig_render(sig_ctx_t *c, TSNode node) { break; } } + TSNode result = sig_field(node, "return_type"); + if (!ts_node_is_null(result) && sig_writes_types(c)) { + sig_raw_str(c, "=>"); + sig_type_tokens(c, result); + } } } @@ -1129,7 +1141,7 @@ const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *sou if (!spelled) { return NULL; } - size_t n = sig_spell(spelled, full); + size_t n = sig_spell(spelled, full, lang); if (n <= CBM_CALLABLE_SIG_MAX) { return sig_roundtrips(spelled, n) ? spelled : sig_hashed(a, spelled, n); } @@ -1140,12 +1152,12 @@ const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *sou int limit = c.too_many ? SIG_MAX_ENTRIES : c.entries; for (int i = 0; i < limit; i++) { char *prefix = cbm_arena_strndup(a, c.buf, c.entry_end[i]); - c.entry_end[i] = prefix ? sig_spell(prefix, c.entry_end[i]) : 0; + c.entry_end[i] = prefix ? sig_spell(prefix, c.entry_end[i], lang) : 0; } char *open_prefix = cbm_arena_strndup(a, c.buf, c.open_off); char *close_prefix = cbm_arena_strndup(a, c.buf, c.close_off); - c.open_off = open_prefix ? sig_spell(open_prefix, c.open_off) : 0; - c.close_off = close_prefix ? sig_spell(close_prefix, c.close_off) : 0; + c.open_off = open_prefix ? sig_spell(open_prefix, c.open_off, lang) : 0; + c.close_off = close_prefix ? sig_spell(close_prefix, c.close_off, lang) : 0; c.buf = spelled; c.len = n; const char *capped = sig_capped(a, &c, spelled, n); @@ -1213,13 +1225,70 @@ size_t cbm_qn_callable_base_len_named(const char *qn, const char *name) { return anchored ? base : len; } +/* Locate only a declaration's return marker, never a nested closure arrow or + * an arrow in generic requirements. Shared by inverse and Swift registry. */ +static bool sig_return_type_valid(const char *type) { + char stack[SIG_DEPTH_LIMIT]; + unsigned depth = 0; + if (!type[0] || strpbrk(type, ".#;{}") || strstr(type, "::") || strstr(type, "->")) { + return false; + } + for (size_t i = 0; type[i]; i++) { + char ch = type[i]; + if (strchr("([<", ch)) { + if (depth == SIG_DEPTH_LIMIT) { + return false; + } + stack[depth++] = ch; + } else if (strchr(")]>", ch) && !(ch == '>' && i > 0 && type[i - 1] == '=')) { + char open = ch == ')' ? '(' : (ch == ']' ? '[' : '<'); + if (!depth || stack[--depth] != open) { + return false; + } + } + } + return depth == 0; +} + +size_t cbm_callable_return_offset(const char *suffix) { + size_t len = strlen(suffix); + if (!strstr(suffix, "=>")) { + return len; + } + int parens = 0, brackets = 0, angles = 0; + for (size_t i = 0; i + 1 < len; i++) { + char ch = suffix[i]; + bool arrow = ch == '=' && suffix[i + 1] == '>'; + bool after_params = i && (suffix[i - 1] == ')' || + (i >= SIG_ASYNC_LEN && + memcmp(suffix + i - SIG_ASYNC_LEN, "async", SIG_ASYNC_LEN) == 0)); + if (arrow && after_params && parens == 0 && brackets == 0 && angles == 0) { + return sig_return_type_valid(suffix + i + SIG_ARROW_LEN) ? i : 0; + } + if (ch == '(') { + parens++; + } else if (ch == ')') { + parens--; + } else if (ch == '[') { + brackets++; + } else if (ch == ']') { + brackets--; + } else if (ch == '<' && (angles > 0 || sig_ident_char((unsigned char)suffix[i + 1]))) { + angles++; + } else if (ch == '>' && angles > 0 && (i == 0 || suffix[i - 1] != '=')) { + angles--; + } + } + return len; +} + size_t cbm_qn_callable_base_len(const char *qn) { if (!qn) { return 0; } size_t len = strlen(qn); /* Trailing cvref (C++): const / volatile / & / && after the ')'. */ - size_t t = len; + size_t t = cbm_callable_return_offset(qn); if (t >= SIG_ASYNC_LEN && memcmp(qn + t - SIG_ASYNC_LEN, "async", SIG_ASYNC_LEN) == 0) { t -= SIG_ASYNC_LEN; } diff --git a/internal/cbm/callable_sig.h b/internal/cbm/callable_sig.h index f41feaa572..d28ed56c52 100644 --- a/internal/cbm/callable_sig.h +++ b/internal/cbm/callable_sig.h @@ -3,10 +3,12 @@ * * A callable's graph identity is * - * callable_qn := base_qn [] (params) [cvref] + * callable_qn := base_qn [] (params) [cvref/async] [=>return_type] * Swift includes generic declarations and where requirements in * (associated-type paths use '/' instead of '.') and appends function-level - * async after params. These distinguish declarations, not call applicability. + * async after params and an explicit return type after "=>". Swift preserves + * qualified type paths using '/'. These distinguish declarations, not call + * applicability. * * so overloads that share a base QN (Java/C#/C++/Kotlin/Swift/Scala/ObjC) * become distinct nodes instead of merging into one node that collects every @@ -25,8 +27,8 @@ * - it is whitespace-normalized: one space only between two identifier * characters ("unsigned long", "const T&"), none elsewhere; * - type spelling: comments, annotations and attributes are dropped; - * qualified type paths keep their last segment (java.util.List -> List, - * std::vector -> vector); "->" in function types is spelled "=>"; a + * except Swift, qualified paths keep their last segment (java.util.List + * -> List, std::vector -> vector); "->" in function types is spelled "=>"; a * variadic/rest marker is "[]" for Java (the JVM array it is) and "~" * everywhere else; C++ top-level cv-qualifiers are dropped and array * parameters decay to pointers, as the language itself does; @@ -80,6 +82,9 @@ const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *sou * name, not a suffix. NULL -> 0. */ size_t cbm_qn_callable_base_len(const char *qn); +/* Top-level declaration return marker; strlen(suffix) if absent, 0 if malformed. */ +size_t cbm_callable_return_offset(const char *suffix); + /* The inverse anchored to the callable's bare `name` (the node's name * column): the suffix is stripped only when the base QN is `name` or ends in * "." / "::" + `name`. Other QN text can end in ')' as well — a Java field diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index f52b2f2b90..cc679d2bb9 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1644,7 +1644,7 @@ static bool swift_parameters_read(const char *qn, const char *name, const swift_ swift_parameters_t *params) { size_t base_len = cbm_qn_callable_base_len_named(qn, name); const char *sig = qn + base_len; - size_t n = strlen(sig); + size_t n = cbm_callable_return_offset(sig); const size_t async_len = sizeof("async") - SKIP_ONE; if (n >= async_len && memcmp(sig + n - async_len, "async", async_len) == 0) { n -= async_len; diff --git a/src/store/store.h b/src/store/store.h index fcf5805114..bae3316521 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -23,7 +23,7 @@ typedef struct cbm_store cbm_store_t; #define CBM_STORE_OK 0 #define CBM_STORE_ERR (-1) #define CBM_STORE_NOT_FOUND (-2) -#define CBM_INDEX_FORMAT_VERSION 3 +#define CBM_INDEX_FORMAT_VERSION 4 #define CBM_STORE_CANCELLED (-3) #define CBM_STORE_SCAN_LIMIT (-4) #define CBM_STORE_CALLBACK_ERR (-5) From 90bfd2e19da6102ff822f42307ee08e63f6ce102 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 16:28:25 +0800 Subject: [PATCH 46/49] style(swift): format overload identity regressions Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 4 +-- tests/test_callable_sig.c | 27 ++++++++++---------- tests/test_index_format.c | 30 +++++++++++------------ tests/test_pipeline.c | 49 +++++++++++++++++++------------------ tests/test_registry.c | 3 ++- 5 files changed, 57 insertions(+), 56 deletions(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index caa774f6ee..efa0b958ad 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -1260,8 +1260,8 @@ size_t cbm_callable_return_offset(const char *suffix) { char ch = suffix[i]; bool arrow = ch == '=' && suffix[i + 1] == '>'; bool after_params = i && (suffix[i - 1] == ')' || - (i >= SIG_ASYNC_LEN && - memcmp(suffix + i - SIG_ASYNC_LEN, "async", SIG_ASYNC_LEN) == 0)); + (i >= SIG_ASYNC_LEN && memcmp(suffix + i - SIG_ASYNC_LEN, "async", + SIG_ASYNC_LEN) == 0)); if (arrow && after_params && parens == 0 && brackets == 0 && angles == 0) { return sig_return_type_valid(suffix + i + SIG_ARROW_LEN) ? i : 0; } diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index a4cc8bcb52..065b054365 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -134,7 +134,8 @@ static const sig_case_t k_cases[] = { "class C { subscript(index i: Int) -> Int { return 0 } }", "subscript_declaration", 0, "(index:Int)=>Int"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, - "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, "(l:C,r:C)=>C"}, + "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, + "(l:C,r:C)=>C"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func extra() {}", "function_declaration", 0, "()"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", "function_declaration", @@ -167,11 +168,10 @@ static const sig_case_t k_cases[] = { "function_declaration", 0, "(_:[A/Item],body:(B/Item)=>A/Item)async=>(A/Item,[B/Item])"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f ( _ x : [ A /* comment */ . Item ], body : ( B.Item ) -> A.Item ) async " - "-> ( A.Item, [ B.Item ] ) {}", "function_declaration", 0, - "(_:[A/Item],body:(B/Item)=>A/Item)async=>(A/Item,[B/Item])"}, - {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, - "func f() -> (A.Item) -> B.Item {}", "function_declaration", 0, - "()=>(A/Item)=>B/Item"}, + "-> ( A.Item, [ B.Item ] ) {}", + "function_declaration", 0, "(_:[A/Item],body:(B/Item)=>A/Item)async=>(A/Item,[B/Item])"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f() -> (A.Item) -> B.Item {}", + "function_declaration", 0, "()=>(A/Item)=>B/Item"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T, _ y: U) -> Int where T == Box<[Int]>, U == () -> Void {}", "function_declaration", 0, ",U==()=>Void>(_:T,_:U)=>Int"}, @@ -263,19 +263,18 @@ TEST(callable_sig_cap_keeps_identity) { cbm_arena_destroy(&a); n = (size_t)snprintf(src_a, sizeof(src_a), "func f() -> ("); for (int i = 0; i < 24; i++) { - n += (size_t)snprintf(src_a + n, sizeof(src_a) - n, "%sA.VeryLongType%d", - i ? "," : "", i); + n += (size_t)snprintf(src_a + n, sizeof(src_a) - n, "%sA.VeryLongType%d", i ? "," : "", i); } memcpy(src_b, src_a, n); snprintf(src_a + n, sizeof(src_a) - n, ",A.Last) {}"); snprintf(src_b + n, sizeof(src_b) - n, ",B.Last) {}"); cbm_arena_init(&a); - sa = sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_a, - "function_declaration", 0); - sa2 = sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_a, - "function_declaration", 0); - sb = sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_b, - "function_declaration", 0); + sa = + sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_a, "function_declaration", 0); + sa2 = + sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_a, "function_declaration", 0); + sb = + sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_b, "function_declaration", 0); ASSERT_NOT_NULL(sa); ASSERT_NOT_NULL(sb); ASSERT_TRUE(strlen(sa) <= CBM_CALLABLE_SIG_MAX); diff --git a/tests/test_index_format.c b/tests/test_index_format.c index 0474e02b9d..8f0c617c0b 100644 --- a/tests/test_index_format.c +++ b/tests/test_index_format.c @@ -282,21 +282,21 @@ TEST(index_format_version_one_rebuilds) { static int swift_collision_migration_case(int scenario) { const char *sources[] = {"func pick(_ x: T) { first() }\n" - "func pick(_ x: T) { second() }\n" - "func first() {}\nfunc second() {}\n" - "func caller() { pick(value) }\n" - "func wrong() { pick(key: 1) }\n", - "func pick(_ x: A.Item) { first() }\n" - "func pick(_ x: B.Item) { second() }\n" - "func first() {}\nfunc second() {}\n" - "func caller() { pick(value) }\n" - "func wrong() { pick(key: 1) }\n" - "enum A { struct Item {} }\nenum B { struct Item {} }\n", - "func pick() -> Int { first(); return 1 }\n" - "func pick() -> String { second(); return \"x\" }\n" - "func first() {}\nfunc second() {}\n" - "func caller() { let _: Int = pick() }\n" - "func wrong() { pick(key: 1) }\n"}; + "func pick(_ x: T) { second() }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { pick(value) }\n" + "func wrong() { pick(key: 1) }\n", + "func pick(_ x: A.Item) { first() }\n" + "func pick(_ x: B.Item) { second() }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { pick(value) }\n" + "func wrong() { pick(key: 1) }\n" + "enum A { struct Item {} }\nenum B { struct Item {} }\n", + "func pick() -> Int { first(); return 1 }\n" + "func pick() -> String { second(); return \"x\" }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { let _: Int = pick() }\n" + "func wrong() { pick(key: 1) }\n"}; const char *legacy_suffix[] = {"pick(_:T)", "pick(_:Item)", "pick()"}; const RFile files[] = {{"Service.swift", sources[scenario]}}; RProj lp; diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index bc7ba1a11c..ad437be394 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8522,7 +8522,7 @@ TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { /* Distinct declarations retain their own source ranges and outgoing edges; * labels/arity alone cannot choose generic constraints or sync vs async. */ static int swift_declaration_identity_case(const char **suffixes, int total, - const char *service_source, const char *caller_source) { + const char *service_source, const char *caller_source) { for (int mode = 0; mode < 2; mode++) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_generic_XXXXXX"); @@ -8618,34 +8618,35 @@ static int swift_declaration_identity_case(const char **suffixes, int total, } TEST(pipeline_swift_generic_async_identity_issue2061) { - const char *suffixes[] = {"pick(_:T)", "pick(_:T)", + const char *suffixes[] = {"pick(_:T)", + "pick(_:T)", "pick(_:T)", - "pick(_:T)", "pick(_:Int)", "pick(_:Int)async"}; - return swift_declaration_identity_case( - suffixes, 6, - "class Service {\n" - "func pick(_ x: T) { mark0() }\n" - "func pick(_ x: T) { mark1() }\n" - "func pick(_ x: T) where T: Hashable { mark2() }\n" - "func pick(_ x: T) where T: Comparable { mark3() }\n" - "func pick(_ x: Int) { mark4() }\n" - "func pick(_ x: Int) async { mark5() }\n}\n" - "func mark0() {}\nfunc mark1() {}\nfunc mark2() {}\n" - "func mark3() {}\nfunc mark4() {}\nfunc mark5() {}\n", - "class Caller {\nlet service = Service()\n" - "func invoke() { self.service.pick(value) }\n}\n"); + "pick(_:T)", + "pick(_:Int)", + "pick(_:Int)async"}; + return swift_declaration_identity_case(suffixes, 6, + "class Service {\n" + "func pick(_ x: T) { mark0() }\n" + "func pick(_ x: T) { mark1() }\n" + "func pick(_ x: T) where T: Hashable { mark2() }\n" + "func pick(_ x: T) where T: Comparable { mark3() }\n" + "func pick(_ x: Int) { mark4() }\n" + "func pick(_ x: Int) async { mark5() }\n}\n" + "func mark0() {}\nfunc mark1() {}\nfunc mark2() {}\n" + "func mark3() {}\nfunc mark4() {}\nfunc mark5() {}\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(value) }\n}\n"); } TEST(pipeline_swift_qualified_and_return_identity_issue2061) { const char *qualified[] = {"pick(_:A/Item)", "pick(_:B/Item)"}; - return swift_declaration_identity_case( - qualified, 2, - "class Service {\nfunc pick(_ x: A.Item) { mark0() }\n" - "func pick(_ x: B.Item) { mark1() }\n}\n" - "func mark0() {}\nfunc mark1() {}\n" - "enum A { struct Item {} }\nenum B { struct Item {} }\n", - "class Caller {\nlet service = Service()\n" - "func invoke() { self.service.pick(value) }\n}\n"); + return swift_declaration_identity_case(qualified, 2, + "class Service {\nfunc pick(_ x: A.Item) { mark0() }\n" + "func pick(_ x: B.Item) { mark1() }\n}\n" + "func mark0() {}\nfunc mark1() {}\n" + "enum A { struct Item {} }\nenum B { struct Item {} }\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(value) }\n}\n"); } TEST(pipeline_swift_return_identity_issue2061) { diff --git a/tests/test_registry.c b/tests/test_registry.c index 5e0a5f10ce..b464d0cd9d 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1345,7 +1345,8 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { generic_call.callee_name = "Service.nested"; ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 1); ASSERT_STR_EQ(out[0], nested); - const char *returns[] = {"proj.Service.pick()=>Int", "proj.Service.pick()async=>(A/Item)=>B/Item"}; + const char *returns[] = {"proj.Service.pick()=>Int", + "proj.Service.pick()async=>(A/Item)=>B/Item"}; for (int i = 0; i < 2; i++) { cbm_registry_add(r, "pick", returns[i], "Method"); cbm_registry_set_swift_signature(r, returns[i], 0, 0); From 7867379f5bf99843f644e4386fdab400820797e7 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 16:30:39 +0800 Subject: [PATCH 47/49] refactor(swift): simplify balanced return validation Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index efa0b958ad..07f424369a 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -35,6 +35,8 @@ enum { static const uint64_t SIG_FNV_OFFSET = 0xcbf29ce484222325ULL; /* FNV-1a 64 basis */ static const uint64_t SIG_FNV_PRIME = 0x100000001b3ULL; static const char SIG_OPERATOR_CHARS[] = "+-*/%^&|~!=<>?[]"; +static const char SIG_TYPE_OPEN[] = "([<"; +static const char SIG_TYPE_CLOSE[] = ")]>"; CBMCallableIdentity cbm_callable_identity(CBMLanguage lang) { /* Each language enables identity with its own index-format bump. */ @@ -1235,14 +1237,21 @@ static bool sig_return_type_valid(const char *type) { } for (size_t i = 0; type[i]; i++) { char ch = type[i]; - if (strchr("([<", ch)) { + if (ch == '>' && i > 0 && type[i - SIG_CHAR_LEN] == '=') { + continue; + } + const char *closing = strchr(SIG_TYPE_CLOSE, ch); + if (strchr(SIG_TYPE_OPEN, ch)) { if (depth == SIG_DEPTH_LIMIT) { return false; } stack[depth++] = ch; - } else if (strchr(")]>", ch) && !(ch == '>' && i > 0 && type[i - 1] == '=')) { - char open = ch == ')' ? '(' : (ch == ']' ? '[' : '<'); - if (!depth || stack[--depth] != open) { + } else if (closing) { + if (!depth) { + return false; + } + depth--; + if (stack[depth] != SIG_TYPE_OPEN[closing - SIG_TYPE_CLOSE]) { return false; } } @@ -1255,11 +1264,13 @@ size_t cbm_callable_return_offset(const char *suffix) { if (!strstr(suffix, "=>")) { return len; } - int parens = 0, brackets = 0, angles = 0; - for (size_t i = 0; i + 1 < len; i++) { + int parens = 0; + int brackets = 0; + int angles = 0; + for (size_t i = 0; i + SIG_CHAR_LEN < len; i++) { char ch = suffix[i]; - bool arrow = ch == '=' && suffix[i + 1] == '>'; - bool after_params = i && (suffix[i - 1] == ')' || + bool arrow = ch == '=' && suffix[i + SIG_CHAR_LEN] == '>'; + bool after_params = i && (suffix[i - SIG_CHAR_LEN] == ')' || (i >= SIG_ASYNC_LEN && memcmp(suffix + i - SIG_ASYNC_LEN, "async", SIG_ASYNC_LEN) == 0)); if (arrow && after_params && parens == 0 && brackets == 0 && angles == 0) { @@ -1273,9 +1284,9 @@ size_t cbm_callable_return_offset(const char *suffix) { brackets++; } else if (ch == ']') { brackets--; - } else if (ch == '<' && (angles > 0 || sig_ident_char((unsigned char)suffix[i + 1]))) { + } else if (ch == '<' && (angles > 0 || sig_ident_char((unsigned char)suffix[i + SIG_CHAR_LEN]))) { angles++; - } else if (ch == '>' && angles > 0 && (i == 0 || suffix[i - 1] != '=')) { + } else if (ch == '>' && angles > 0 && (i == 0 || suffix[i - SIG_CHAR_LEN] != '=')) { angles--; } } From 10f4769779c8ec63d509b7884219187a0e7a1939 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 16:33:33 +0800 Subject: [PATCH 48/49] style(swift): wrap generic boundary guard Signed-off-by: DavidHLP --- internal/cbm/callable_sig.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 07f424369a..b5ff645ae1 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -1284,7 +1284,8 @@ size_t cbm_callable_return_offset(const char *suffix) { brackets++; } else if (ch == ']') { brackets--; - } else if (ch == '<' && (angles > 0 || sig_ident_char((unsigned char)suffix[i + SIG_CHAR_LEN]))) { + } else if (ch == '<' && + (angles > 0 || sig_ident_char((unsigned char)suffix[i + SIG_CHAR_LEN]))) { angles++; } else if (ch == '>' && angles > 0 && (i == 0 || suffix[i - SIG_CHAR_LEN] != '=')) { angles--; From 35533b2674a09e2e6e9a066faf52e3fb8ccdaada Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Mon, 5 Oct 2026 17:31:39 +0800 Subject: [PATCH 49/49] docs(swift): record qualified and return identity validation Signed-off-by: DavidHLP --- docs/SWIFT_IDENTITY_VALIDATION.md | 44 ++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md index fa6117ef41..a485da4b03 100644 --- a/docs/SWIFT_IDENTITY_VALIDATION.md +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -23,10 +23,46 @@ The TS/HTML/SCSS rebuild regression remains. ## P2 repair evidence Review checkpoint: `500707c5b853da75a0a4fc7f1ae3b9364c04792e`. The previous -generic/where/async, negative-target, migration and documentation findings remain -resolved. New qualified-type and return-only collisions require independent -RED/GREEN and final-source verification; the historical passes below do not -establish acceptance of this repair. +generic/where/async, negative-target, migration and documentation findings are +Already Addressed. Complete test-only RED at +`e3cd15a9c462cb294eafcc9ea39cbef90fafe22f` leaves production unchanged and +independently reproduces both qualified-parameter and return-only collisions: +one node instead of two. It exits 1; failed-assert cleanup also reports leaks. + +Tested repair source: `10f4769779c8ec63d509b7884219187a0e7a1939`. +Build/test/lint ran on remote-dev with the environment listed below. Subsequent +evidence-only commits do not imply reruns at their SHA. All times are UTC on +2026-10-05. + +| Command/path | Time | Exit/result | +| --- | --- | --- | +| Seven focused suites listed below | 08:52:55–09:27:22 | 0; 909 passes, two ObjectScript UBSan reports; **not sanitizer-clean** | +| `scripts/test.sh --tsan TEST_TSAN_SUITES=pipeline` | 08:47:26–09:16:01 | 0; 328 passes | +| Strict graph / real JSON-RPC MCP drivers | 09:20:01–09:20:24 | 0 / 0; 10/10 and 5/5 observations, no sanitizer reports | +| CI lint, unchanged rules with Cppcheck `-j4` | 09:02:08–09:16:35 | 0; complete retry after retained 900s timeout/124 | +| `scripts/build.sh BUILD_DIR=build/p2-prod` | 08:38:32–08:56:39 | 0; isolated clean production build | +| Production `--version` / `--help` | 08:57:04 | 139 / 139; same startup stack as historical main control | +| `scripts/test.sh`, real stable tags | 08:34:54–08:36:29 | 1; Scoop metadata preflight, full C suite not reached | +| Targeted `lint-tidy` on signature/registry | 09:06:39–09:06:45 | 2; existing diagnostics remain; not a full lint pass | +| Formatter/whitespace/DCO range | 08:38:33–08:38:38 | 0; 48 nonmerge commits signed off at tested source | + +The new pipeline cases verify separate nodes/source positions/outgoing edges, +all compatible candidate edges/counts, workers 1/4 and persisted incremental +repair. Format 3 collision/stale-CALLS migration and the retained cross-language +rebuild regression pass. Operator, nested generic requirements, return parsing +and capped-identity regressions also pass. Full-context independent static +reviews found no unresolved material findings; static review is not execution. + +[Public readable summary, complete logs and SHA/time/exit metadata](https://gist.github.com/DavidHLP/a10894c5c6926dc7c0dd874b3998dded) +include RED/GREEN, all five MCP responses, current and historical log tails, +binary hashes and a raw-log checksum manifest. Clone the gist for complete files +when web previews truncate. Targeted old-production lint also reports inverse +complexity 42; new return helpers have no remaining targeted diagnostics. Full +clang-tidy is not green. Runtime report attribution remains unconfirmed; current +macOS/Windows and Ubuntu production CLI semantics remain unexecuted. The tagless +diagnostic full-test timeout and interrupted duplicate-object driver build are +retained separately from completed gates. Maintainer identity-format agreement, +upstream checks and approval remain required before merge. ## Previous repair evidence (historical)