diff --git a/docs/SWIFT_IDENTITY_VALIDATION.md b/docs/SWIFT_IDENTITY_VALIDATION.md new file mode 100644 index 000000000..a485da4b0 --- /dev/null +++ b/docs/SWIFT_IDENTITY_VALIDATION.md @@ -0,0 +1,154 @@ +# Swift identity validation + +Scope: issue #2061 / PR #2436, Swift callable identity and conservative CALLS +candidates only. Bare stored names remain unchanged. Generic declarations, +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. + +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. + +## P2 repair evidence + +Review checkpoint: `500707c5b853da75a0a4fc7f1ae3b9364c04792e`. The previous +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) + +Review checkpoint: `39d5db48a83f94641fccbb44c8cbc0233b5e2a51`, upstream base +`268a9d8886642eb7f9b2ce45f5ce27cdecf0f519`. Regression-only RED checkpoint: +`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). +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/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 9d32f391a..b5ff645ae 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 @@ -22,20 +23,29 @@ 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_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 ")" */ + SIG_CHAR_LEN = 1, + SIG_SWIFT_DEFAULT_BITS = 64, }; 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) { - /* 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. */ + /* Each language enables identity with its own index-format bump. */ + if (lang == CBM_LANG_SWIFT) { + return CBM_CALLABLE_ID_LABELED_TYPED; + } + /* Other languages keep their historical QN until their own enable changes + * land with an index-format bump. Planned modes: Java/Kotlin/C#/C++/CUDA/ + * Scala TYPED, ObjC LABELED, dynamic tier-2 languages ARITY. */ (void)lang; return CBM_CALLABLE_ID_NONE; } @@ -79,7 +89,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; } @@ -737,6 +749,10 @@ 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 +786,74 @@ 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 - SIG_CHAR_LEN] == '>') { + c->buf[--c->len] = '\0'; + } + } else { + sig_raw_str(c, "<"); + } + if (!ts_node_is_null(requirements)) { + sig_raw_str(c, ";"); + sig_type_tokens(c, requirements); + } + sig_raw_str(c, ">"); + for (size_t i = start; i < c->len; i++) { + if (c->buf[i] == '.') { + c->buf[i] = '/'; + } + } +} + +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 = -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 < SIG_SWIFT_DEFAULT_BITS && sig_swift_parameter_has_default(ch)) { + defaults |= UINT64_C(1) << last; + } + } 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; + } + } + 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 > SIG_SWIFT_DEFAULT_BITS ? UINT8_MAX : (uint8_t)parameters; + } + return defaults; +} /* ── Scala ─────────────────────────────────────────────────────── */ static void sig_scala_params(sig_ctx_t *c, TSNode node) { @@ -852,10 +936,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) { @@ -866,6 +951,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) { @@ -958,6 +1048,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); @@ -986,6 +1078,22 @@ 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_str(c, "async"); + 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); + } + } } static bool sig_roundtrips(const char *suffix, size_t n) { @@ -1035,7 +1143,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); } @@ -1046,12 +1154,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); @@ -1119,13 +1227,83 @@ 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 (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 (closing) { + if (!depth) { + return false; + } + depth--; + if (stack[depth] != SIG_TYPE_OPEN[closing - SIG_TYPE_CLOSE]) { + 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; + 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 + 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) { + 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 + SIG_CHAR_LEN]))) { + angles++; + } else if (ch == '>' && angles > 0 && (i == 0 || suffix[i - SIG_CHAR_LEN] != '=')) { + 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; + } for (;;) { if (t > 0 && qn[t - 1] == '&') { t--; @@ -1171,7 +1349,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 - SIG_CHAR_LEN] != '=')) { adepth++; } else if (qn[q] == '<') { adepth--; diff --git a/internal/cbm/callable_sig.h b/internal/cbm/callable_sig.h index c7b4d64fc..d28ed56c5 100644 --- a/internal/cbm/callable_sig.h +++ b/internal/cbm/callable_sig.h @@ -3,7 +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 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 @@ -22,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; @@ -38,6 +43,7 @@ #include "arena.h" #include "tree_sitter/api.h" #include +#include typedef enum { CBM_CALLABLE_ID_NONE = 0, /* historical base QN, no suffix */ @@ -49,20 +55,24 @@ typedef enum { 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. */ +/* The identity mode a language uses for callable QNs. Languages remain + * CBM_CALLABLE_ID_NONE until their enable change lands with an index-format + * bump; Swift is enabled by #2061. 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); +/* 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. */ 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. */ +/* Same, with an explicit mode — the builder's golden tables exercise modes + * independently of each language's production enablement. */ const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *source, CBMLanguage lang, CBMCallableIdentity mode); @@ -72,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/internal/cbm/cbm.h b/internal/cbm/cbm.h index 7982610a3..c0af3d399 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -271,6 +271,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; CBMTestDefinitionRole test_role; /* NONE preserves legacy helper/test-file semantics */ /* Configured raw definitions only; zero for all legacy rows. Exact spans * are bound to the owning result's source identity before cross-file use. */ @@ -287,6 +290,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 @@ -332,6 +338,20 @@ 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; + /* 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; // What an import statement names, when the syntax says so. Only PHP's diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 90bd5c604..e26ead2e6 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -2240,6 +2240,7 @@ static void process_keyword_arg(CBMExtractCtx *ctx, TSNode arg_node, CBMCallArg } } +static TSNode swift_argument_value(TSNode arg); static const char *extract_string_value(CBMExtractCtx *ctx, TSNode val_node); /* A JS/TS request-config object -- `http({url: `/users/${id}`, method: 'GET'})`, @@ -2314,6 +2315,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 - SKIP_ONE] == ':') { + ca->keyword = cbm_arena_strndup(ctx->arena, ca->keyword, n - SKIP_ONE); + } + } + } + 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++; @@ -3581,6 +3602,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++) { @@ -4198,6 +4287,23 @@ 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)) { + swift_capture_trailing_closures(ctx, node, suffix, &call); + } + uint32_t value_arg_count = 0; + if (!ts_node_is_null(args)) { + uint32_t named_count = ts_node_named_child_count(args); + for (uint32_t ai = 0; ai < named_count; ai++) { + TSNode arg = ts_node_named_child(args, ai); + value_arg_count += strcmp(ts_node_type(arg), "value_argument") == 0; + } + } + if (value_arg_count > 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, call.callee_name); /* #1146: a Laravel registration's slashless URI ('users', '') diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 671a81651..6528f4336 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" @@ -6378,6 +6379,23 @@ 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) { + if (!def.qualified_name) { + return; + } + 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.test_role == CBM_TEST_ROLE_CASE) { def.is_test = true; @@ -7722,6 +7740,21 @@ 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) { + if (!method_qn) { + return; + } + 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 4e83ca900..02b8efcc0 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_* @@ -1099,6 +1100,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/internal/cbm/result_compact.c b/internal/cbm/result_compact.c index ee368896a..8cd9b8496 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/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 426bdafc0..e5f98b5de 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -11,6 +11,9 @@ * 2. Method suffix → determines HTTP method (get→GET, post→POST) */ #include "service_patterns.h" +#include "callable_sig.h" +#include "foundation/constants.h" +#include "foundation/mem_core.h" #include #include @@ -1071,25 +1074,32 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) { } } + size_t base_len = cbm_qn_callable_base_len(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 + SKIP_ONE); + if (!base_qn) { + return CBM_SVC_NONE; + } + memcpy(base_qn, resolved_qn, base_len); + base_qn[base_len] = '\0'; + 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(resolved_qn, route_reg_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, http_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, async_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, config_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, grpc_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, graphql_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, 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) { char *kdup = strdup(resolved_qn); diff --git a/src/pipeline/lsp_surface.c b/src/pipeline/lsp_surface.c index 4c3cde336..4be2d14b8 100644 --- a/src/pipeline/lsp_surface.c +++ b/src/pipeline/lsp_surface.c @@ -17,8 +17,10 @@ * 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 #include #include #include @@ -125,6 +127,28 @@ static char *surface_file_to_json(const CBMFileResult *result, const CBMLSPDef * } yyjson_mut_obj_add_val(doc, root, "reg", reg); + yyjson_mut_val *swift = NULL; + for (int i = 0; result && i < result->defs.count; i++) { + const CBMDefinition *d = &result->defs.items[i]; + if (!d->qn_sig_off || !d->qualified_name) { + continue; + } + if (!swift) { + swift = yyjson_mut_arr(doc); + } + yyjson_mut_val *o = yyjson_mut_obj(doc); + yyjson_mut_obj_add_str(doc, o, "q", d->qualified_name); + 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); + } + if (swift) { + yyjson_mut_obj_add_val(doc, root, "swift", swift); + } + /* #1916: an axios instance binding's baseURL is consumed by the files * that import it (their HTTP_CALLS compose base + path), so a changed * base must change the surface or those importers keep stale edges. diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 0d3fdf1cd..2e4925f4b 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 @@ -754,15 +756,45 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM } } - /* #2053: a Rust call the LSP placed on an EXTERNAL symbol (std's - * Path::join, a seeded crate API) is resolved — it just has no graph node. - * The textual registry would bind it to a same-named project method - * instead, so skip it and let the empty-resolution service fallbacks below - * classify the call. MUST match pass_parallel.c. */ + /* 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]; + 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 < 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 = 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); + emitted++; + } + } + return emitted > 0 ? SKIP_ONE : 0; + } + } + + /* A Rust call the LSP placed on an EXTERNAL symbol has no graph node. The + * textual registry must not bind it to a same-named project method. */ bool rust_external = lsp && cbm_pipeline_rust_external_target( lang, lsp->strategy, lsp->callee_qn, ctx->project_name); cbm_resolution_t res = {0}; - if (!rust_external) { + if (swift_candidates >= 0 && !rust_external) { res = cbm_registry_resolve(ctx->registry, call->callee_name, module_qn, imp_keys, imp_vals, imp_count); /* Cross-language veto: a name-only guess never binds another diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 113c25228..0358b9ae4 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 @@ -290,6 +291,11 @@ 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 > 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); + } 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); @@ -402,6 +408,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_lang(ctx->registry, def->name, def->qualified_name, def->label, lang); + 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 fef5e4f54..c95a8ed19 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 }; @@ -517,6 +520,11 @@ 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 > 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); + } 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); @@ -1739,6 +1747,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_lang(ctx->registry, def->name, def->qualified_name, def->label, lang); + 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); @@ -3289,6 +3301,39 @@ 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 == 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, + imp_count, false, route_mount); + ws->calls_resolved++; + } + 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}; + } + } + /* A call that starts another program (subprocess.run, exec.Command, * posix_spawn): a SPAWNS edge to its Process node instead of a CALLS * edge. `res` holds the LSP answer when there is one, as the diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index fd2233fc9..8b974d02d 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 */ @@ -240,6 +241,14 @@ 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); /* As cbm_registry_add, recording `lang`: the DETECTED language of the file * that defines the symbol (CBM_LANG_COUNT = unknown), which the cross-language * veto (cbm_registry_name_guess_vetoed) checks against the caller. */ diff --git a/src/pipeline/pipeline_delta.c b/src/pipeline/pipeline_delta.c index 8634a633e..b01f45a29 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -287,12 +287,8 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g * UNIQUE violation the pre-remap patch would have raised. A resolver that * only LOOKS UP still needs its target resident, which is why the list * mirrors the registry's own membership rule instead of guessing. */ - /* The one property a resolver READS off a proxy: an axios instance - * binding's client + baseURL (#1916). A caller re-resolved alone must - * compose `api.get('/p')` against its unchanged wrapper exactly as a full - * build does. Only the two keys, only on the rare Module/Variable rows - * that carry them; every other proxy stays "{}". Proxies are never - * written back (cbm_delta_patch skips id <= max_db_id). */ + /* Preserve the resolver-visible HTTP and Swift signature properties on + * proxies; proxy nodes are never written back to the store. */ sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2( db, @@ -300,7 +296,10 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g " CASE WHEN label IN ('Module','Variable')" " AND instr(properties, '\"http_client\"') > 0" " THEN json_object('http_client', json_extract(properties, '$.http_client')," - " 'http_base_url', json_extract(properties, '$.http_base_url')) END" + " 'http_base_url', json_extract(properties, '$.http_base_url'))" + " WHEN instr(properties, '\"swift_defaults\"') > 0" + " THEN json_object('swift_defaults', json_extract(properties, '$.swift_defaults')," + " 'swift_params', json_extract(properties, '$.swift_params')) END" " FROM nodes" " WHERE project = ?1 AND label NOT IN" " ('Macro','Comment','Section','Branch','Commit','Tag')" diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index b60f8bdec..24c5741c7 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1278,6 +1278,23 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { } } cbm_registry_add_lang(seed->registry, node->name, node->qualified_name, node->label, lang); + 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, 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(seed->registry, node->qualified_name, + (uint64_t)defaults, (uint8_t)params); + } + } + } } /* Seed the registry from every registry symbol in the graph buffer, with diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 096b44ec9..493579cbc 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -30,11 +30,13 @@ 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 */ #include #include +#include #include #include @@ -91,6 +93,10 @@ typedef struct { uint8_t *lang; } qn_array_t; +typedef struct { + uint64_t defaults; + uint8_t count; +} swift_signature_t; _Static_assert(CBM_LANG_COUNT <= UINT8_MAX, "CBMLanguage must fit the per-entry lang byte"); struct cbm_registry { @@ -104,6 +110,7 @@ struct cbm_registry { /* byName: simpleName → qn_array_t* (heap-owned) */ CBMHashTable *by_name; + CBMHashTable *swift_signatures; /* borrowed exact-map keys; owned values */ }; /* ── Helpers ─────────────────────────────────────────────────────── */ @@ -1078,6 +1085,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; @@ -1085,6 +1098,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++) { @@ -1219,6 +1236,33 @@ void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qual } } +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) { @@ -1769,6 +1813,425 @@ 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. */ +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[-SKIP_ONE] == '?') { + end--; + continue; + } + if (!swift_type_wraps(start, end)) { + break; + } + start++; + end--; + } + 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; + } + swift_parameter_depth_step(&depth, *p, p == start ? '\0' : p[-SKIP_ONE]); + } + return false; +} + +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; + +/* 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; +} + +/* 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 = 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; + } + /* Declaration generic requirements distinguish nodes, but do not prove + * applicability. Read only params when selecting compatible candidates. */ + if (n && sig[0] == '<') { + size_t i = swift_generic_prefix_len(sig, n); + if (i == 0 || i >= n) { + return false; + } + sig += i; + n -= i; + } + if (n < PAIR_LEN || sig[0] != '(' || sig[n - SKIP_ONE] != ')' || strchr(sig, '#')) { + return false; + } + 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++) { + 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 + SKIP_ONE; + continue; + } + char previous = p == entry ? '\0' : p[-SKIP_ONE]; + swift_parameter_depth_step(&depth, *p, previous); + } + 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 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 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. */ + unsigned trailing_total = call->swift_trailing_count; + if (trailing_total == 0 && call->swift_trailing_closure) { + trailing_total = SKIP_ONE; + } + /* 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] = {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; + } + swift_parameter_advance(params, meta, call, i, trailing_total, arg, states[arg], next); + } + memcpy(states, next, sizeof(states)); + } + 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->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; + 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 < 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) { + 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]; + 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 ? SWIFT_INCOMPATIBLE : 0; + } + const char *chosen = + swift_choose_candidate(matches, matched, name, call, module_qn, import_vals, import_count); + 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 SWIFT_INCOMPATIBLE; + } + out[found++] = matches[i]; + } + } + return found; +} + /* ── Class-qualified route handlers (PHP, #1146) ─────────────────── */ /* Method QNs are path-based: ..... @@ -2052,14 +2515,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; } diff --git a/src/store/store.h b/src/store/store.h index 3cea85b90..46e06824c 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -24,7 +24,7 @@ typedef struct cbm_store_read_scope cbm_store_read_scope_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 4 #define CBM_STORE_CANCELLED (-3) #define CBM_STORE_SCAN_LIMIT (-4) #define CBM_STORE_CALLBACK_ERR (-5) diff --git a/tests/repro/issue2061_swift_identity/README.md b/tests/repro/issue2061_swift_identity/README.md new file mode 100644 index 000000000..20d7ab436 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/README.md @@ -0,0 +1,37 @@ +# Swift overload reproduction (#2061) + +Ordinary regressions use the repository entry point: + +```sh +scripts/test.sh --suites extraction,callable_sig,registry,pipeline,index_format +``` + +`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" +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 +``` + +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. + +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/driver.c b/tests/repro/issue2061_swift_identity/driver.c new file mode 100644 index 000000000..6505a6e9e --- /dev/null +++ b/tests/repro/issue2061_swift_identity/driver.c @@ -0,0 +1,22 @@ +#define main graph_main +#define passed graph_passed +#define failed graph_failed +#define errors graph_errors +#include "main.c" +#undef main +#undef passed +#undef failed +#undef errors +#define main mcp_main +#include "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/main.c b/tests/repro/issue2061_swift_identity/main.c new file mode 100644 index 000000000..b98b6b5ec --- /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. + * Retain the temporary repository and DB as evidence; see README.md. + */ +#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/mcp_driver.c b/tests/repro/issue2061_swift_identity/mcp_driver.c new file mode 100644 index 000000000..b34ffb160 --- /dev/null +++ b/tests/repro/issue2061_swift_identity/mcp_driver.c @@ -0,0 +1,207 @@ +/* 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. + */ +#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) { + /* 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 " + "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; +} diff --git a/tests/repro/issue2061_swift_identity/repro.mk b/tests/repro/issue2061_swift_identity/repro.mk new file mode 100644 index 000000000..f328fa741 --- /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/repro_call_argument_matrix_a.c b/tests/repro/repro_call_argument_matrix_a.c index 5399bddab..c3ef6023b 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; diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index ad0315032..065b05436 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -1,6 +1,5 @@ /* - * test_callable_sig.c — golden tables for the signature-qualified callable - * identity builder and its inverse (#2061, plumbing: no language enabled). + * test_callable_sig.c — golden tables for signature-qualified callable identity (#2061). */ #include "test_framework.h" #include "callable_sig.h" @@ -123,8 +122,9 @@ 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," - "xs:Int~,io:inout Int,t:(a:Int,String))"}, + "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(" + "Int)=>Void," + "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,11 +132,52 @@ 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", + 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"}, + {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 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"}, + {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() -> (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: " @@ -220,16 +261,39 @@ 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(); } -/* 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 +308,27 @@ 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[] = { @@ -267,6 +352,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])); @@ -290,6 +378,13 @@ 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.<(_: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"}, {"p.O.initWithFrame(_:style:)", "p.O.initWithFrame"}, {"p.C.+(l:C,r:C)", "p.C.+"}, @@ -340,7 +435,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_extraction.c b/tests/test_extraction.c index f387b21ef..7e1694ae4 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -2308,6 +2308,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"); @@ -9461,6 +9501,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 @@ -9743,6 +9874,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_index_format.c b/tests/test_index_format.c index ceec7b370..8f0c617c0 100644 --- a/tests/test_index_format.c +++ b/tests/test_index_format.c @@ -243,7 +243,175 @@ TEST(index_format_legacy_index_rebuilds_and_repairs) { PASS(); } +TEST(index_format_version_one_rebuilds) { + RProj lp; + cbm_store_t *initial = rh_index_files(&lp, k_files, k_nfiles); + ASSERT_NOT_NULL(initial); + cbm_store_close(initial); + + cbm_store_t *writer = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(writer); + ASSERT_EQ(cbm_store_set_format_version(writer, 1), CBM_STORE_OK); + cbm_store_close(writer); + + char *resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(g_log_buf, "format_change_reindex")); + ASSERT_NOT_NULL(strstr(resp, "\"format_migration\":true")); + free(resp); + + cbm_store_t *after_migration = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(after_migration); + int format = -1; + ASSERT_EQ(cbm_store_get_format_version(after_migration, &format), CBM_STORE_OK); + ASSERT_EQ(format, CBM_INDEX_FORMAT_VERSION); + ASSERT_EQ(rh_count_label(after_migration, lp.project, "File"), k_nfiles); + cbm_store_close(after_migration); + + resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + ASSERT_NULL(strstr(g_log_buf, "format_change_reindex")); + ASSERT_NULL(strstr(resp, "format_migration")); + free(resp); + cbm_store_t *settled = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(settled); + ASSERT_EQ(rh_count_label(settled, lp.project, "File"), k_nfiles); + rh_cleanup(&lp, settled); + PASS(); +} + +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"}; + 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); + 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.%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", + .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, scenario ? 3 : 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_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); + 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(); +} + +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); + RUN_TEST(index_format_version_one_rebuilds); + RUN_TEST(index_format_swift_collisions_and_stale_calls_rebuild); } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 481738fe8..581fd2c79 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -9055,6 +9055,709 @@ 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 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); + 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); + 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)", 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); + snprintf(many_caller_qn, sizeof(many_caller_qn), + "%s.Sources.Caller.Caller.callManyWithComment()", project); + cbm_node_t record = {0}, record_caller = {0}, many = {0}, many_caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, record_qn, &record), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, record_caller_qn, &record_caller), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, many_qn, &many), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, many_caller_qn, &many_caller), CBM_STORE_OK); + 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); + 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); + cbm_node_free_fields(&many_caller); + 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(); +} + +/* 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) { + 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", service_source); + 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); + 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); + 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); + 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, 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 < 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); + 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, total); + int compatible = 0; + for (int e = 0; e < n; e++) { + if (edges[e].target_id == target.id) { + char candidates[64]; + snprintf(candidates, sizeof(candidates), "\"candidates\":%d", total); + ASSERT_NOT_NULL(strstr(edges[e].properties_json, candidates)); + 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(); +} + +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)"}; + 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) { + 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]; + 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" + " 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" + " 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); + 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); + char record_qn[512]; + snprintf(record_qn, sizeof(record_qn), + "%s.Sources.Service.Service.record(path:String,session:URLSession)", project); + cbm_node_t caller = {0}, data = {0}, url = {0}, record = {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(cbm_store_find_node_by_qn(s, project, record_qn, &record), 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); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, record.id), 1); + ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 0); + 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); + + /* #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); + cbm_node_free_fields(&record); + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + 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 — + * 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). */ +static int swift_incompatible_overload_fixture(bool parallel) { + 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" + "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"); + 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); + 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", 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); + 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); + 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); + /* 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); + 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(); +} + +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 @@ -17973,6 +18676,124 @@ TEST(pipeline_objectscript_export_range_join_keeps_one_trailing_marker) { } #endif +static bool swift_default_restore_graph_is_valid(cbm_store_t *store, const char *project, + bool has_default) { + char service_qn[512], good_qn[512], bad_qn[512]; + snprintf(service_qn, sizeof(service_qn), "%s.Sources.Service.Service.ping(a:Int,c:Int)", + project); + snprintf(good_qn, sizeof(good_qn), "%s.Sources.Caller.Caller.good()", project); + snprintf(bad_qn, sizeof(bad_qn), "%s.Sources.Caller.Caller.bad()", project); + cbm_node_t service = {0}, good = {0}, bad = {0}; + bool valid = cbm_store_find_node_by_qn(store, project, service_qn, &service) == CBM_STORE_OK && + cbm_store_find_node_by_qn(store, project, good_qn, &good) == CBM_STORE_OK && + 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, "\"swift_params\":2") && + pipeline_has_calls_edge(store, good.id, service.id) == has_default && + pipeline_has_calls_edge(store, bad.id, service.id) == 0; + } + cbm_node_free_fields(&service); + cbm_node_free_fields(&good); + cbm_node_free_fields(&bad); + return valid; +} + +TEST(pipeline_swift_incremental_restores_default_signature_issue2061) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_restore_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " func ping(a: Int, c: Int = 0) {}\n" + " func other(a: Int, b: Int = 0) {}\n" + "}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + " func good() { self.service.ping(a: 1) }\n" + " func bad() { self.service.ping(x: 1) }\n" + "}\n"); + 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); + + cbm_pipeline_t *first = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(first); + ASSERT_EQ(cbm_pipeline_run(first), 0); + const char *project_name = cbm_pipeline_project_name(first); + ASSERT_NOT_NULL(project_name); + snprintf(project, sizeof(project), "%s", project_name); + cbm_store_t *store = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(store); + ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, true)); + cbm_store_close(store); + cbm_pipeline_free(first); + + FILE *caller = fopen(caller_path, "ab"); + ASSERT_NOT_NULL(caller); + ASSERT_TRUE(fputs("// incremental marker\n", caller) >= 0); + ASSERT_EQ(fclose(caller), 0); + ASSERT_EQ(pipeline_test_set_mtime(caller_path, 2000000000, 0), 0); + cbm_pipeline_incremental_test_reset_faults(); + cbm_pipeline_incremental_test_force_legacy_partial_once(); + cbm_pipeline_t *legacy = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(legacy); + ASSERT_EQ(cbm_pipeline_run(legacy), 0); + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); + store = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(store); + ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, true)); + 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, 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(closure_db_path); + ASSERT_NOT_NULL(store); + ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, true)); + cbm_store_close(store); + cbm_pipeline_free(closure); + + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " func ping(a: Int, c: Int) {}\n" + " 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, 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(closure_db_path); + ASSERT_NOT_NULL(store); + ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, false)); + cbm_store_close(store); + cbm_pipeline_free(defaults_removed); + cbm_pipeline_incremental_test_reset_faults(); + th_rmtree(tmp); + PASS(); +} + /* #1153: a C++ method called on the object a static factory returns * (`SecdManager::getInstance()->m()`, `Registry::instance().m()`, * `auto w = Widget::create(); w->m()`) got no CALLS edge when the class lives @@ -18472,6 +19293,15 @@ 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_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); + RUN_TEST(pipeline_swift_incremental_restores_default_signature_issue2061); RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls); RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http); RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin); diff --git a/tests/test_registry.c b/tests/test_registry.c index bcd589a7d..04076cb2c 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1509,6 +1509,381 @@ 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)", + "proj.Service.consume(values:[()=>Void])", + "proj.Service.consume(completion:()=>Void)", + "proj.Service.accept(value:Pair<()=>Void,Int>)", + "proj.Service.perform(completion:(()=>Void)?)", + "proj.Service.perform(completion:((Int)=>Void))", + "proj.Service.perform(completion:(()=>Void,Int))", + "proj.Service.perform(completion:(()=>Result)?)", + "proj.Service.collect(_:Int~)", + "proj.Service.named(name:Int~)", + "proj.Service.run(completion:()=>Void,flag:Bool)", + "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", "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 || i >= 19 ? UINT64_C(1) + : 0; + cbm_registry_set_swift_signature(r, qns[i], defaults, counts[i]); + } + + 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); + 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"); + 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), 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); + 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[1].keyword = "unexpected"; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.arg_count = 0; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.arg_count = 1; + call.swift_trailing_closure = true; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.swift_trailing_closure = false; + + args[1].keyword = NULL; + 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]); + + /* 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; + 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]); + call.callee_name = "Service.consume"; + call.arg_count = 0; + 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[7]); + call.callee_name = "Service.accept"; + call.arg_count = 1; + call.swift_trailing_closure = false; + args[0].keyword = "value"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[8]); + call.callee_name = "Service.perform"; + call.arg_count = 0; + call.swift_trailing_closure = true; + n = cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8); + ASSERT_EQ(n, 3); /* wrappers transparent; tuple must not match */ + bool has_optional = false, has_grouped = false, has_generic_result = false; + for (int i = 0; i < n; i++) { + has_optional |= strcmp(out[i], qns[9]) == 0; + has_grouped |= strcmp(out[i], qns[10]) == 0; + has_generic_result |= strcmp(out[i], qns[12]) == 0; + } + ASSERT_TRUE(has_optional && has_grouped && has_generic_result); + + call.callee_name = "run"; + call.arg_count = 0; + 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[15]); /* trailing closure before defaulted parameter */ + 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); + ASSERT_STR_EQ(out[0], qns[17]); /* skip default closure for required trailing closure */ + call.callee_name = "combine"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[18]); /* skip variadic closures for required trailing closure */ + call.swift_trailing_closure = false; + args[1].keyword = NULL; + call.callee_name = "collect"; + call.arg_count = 0; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + args[0].keyword = NULL; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + call.arg_count = 3; + args[2].keyword = NULL; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + args[0].keyword = "name"; + call.callee_name = "named"; + call.arg_count = 1; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + args[1].keyword = NULL; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + args[0].keyword = "other"; + call.arg_count = 1; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + args[0].keyword = "name"; + args[1].keyword = "name"; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + cbm_registry_free(r); + PASS(); +} + +TEST(swift_receiver_resolution_is_independent_of_registration_order) { + const char *alpha = "proj.alpha.Service.work(flag:Bool)"; + const char *beta = "proj.beta.Service.work(flag:Bool)"; + CBMCallArg arg = {.keyword = "flag"}; + CBMCall call = {.callee_name = "Service.work", .args = &arg, .arg_count = 1}; + for (int reverse = 0; reverse < 2; reverse++) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + const char *first = reverse ? beta : alpha; + const char *second = reverse ? alpha : beta; + cbm_registry_add(r, "work", first, "Method"); + cbm_registry_add(r, "work", second, "Method"); + cbm_registry_set_swift_signature(r, first, 0, 1); + cbm_registry_set_swift_signature(r, second, 0, 1); + + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.alpha.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], alpha); + cbm_registry_free(r); + } + 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); @@ -1546,6 +1921,13 @@ 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_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);