Repository navigation
Swift: overloaded methods collapse into one node, producing fake callers in trace_path #2061
Description
Version
0.10.8
Platform
macOS (Apple Silicon), Darwin 25.6.0
Summary
Two Swift methods on the same type that differ only in their parameter labels end up as a single node in the graph. The node that survives keeps one overload's line range, but it ends up holding the CALLS edges from both of them.
The practical fallout is that trace_path reports callers that don't exist. A function that only ever calls one overload shows up as a caller of whatever the other overload calls.
Overloading by argument label is pretty common in Swift (work(flag:) vs work(name:)), so this comes up a lot in real code.
Minimal reproduction
Three files, no dependencies, nothing to build.
Sources/Sink.swift
class Sink {
func target() {}
}Sources/Service.swift
class Service {
let sink = Sink()
// Overload A: DOES call target()
func work(flag: Bool) {
self.sink.target()
}
// Overload B: does NOT call target()
func work(name: String) {
print(name)
}
}Sources/Caller.swift
class Caller {
let service = Service()
// Calls ONLY overload B, which never reaches target()
func onlyCallsOverloadB() {
self.service.work(name: "x")
}
}codebase-memory-mcp cli index_repository --repo_path <repro> --name overload-repro --mode fast
codebase-memory-mcp cli search_graph --project overload-repro --name_pattern '^work$'
codebase-memory-mcp cli trace_path --project overload-repro --function_name target --direction inbound --depth 3
Actual
search_graph finds one node where the source has two methods:
total: 1
work Method 10-12 1 2
Lines 10-12 are overload B, work(name:). Overload A at lines 5-7 gets no node at all. But the surviving node reports out 2, and the body of work(name:) is just print(name), so it has picked up the call to target() from the overload that no longer exists.
trace_path then reports a caller that isn't real:
function: target
direction: inbound
callers_total: 2
onlyCallsOverloadB 2 <- should not be here
work 1
onlyCallsOverloadB calls work(name:), and that body is a single print, so it never reaches target() at any depth.
Expected
Two separate nodes, one per overload, with target() reachable only from work(flag:).
Notes
parse_partial_count is 0 for this repo, so nothing is failing to parse and this isn't a grammar issue. It looks like the qualified name doesn't include the parameter labels that tell Swift overloads apart, so the second declaration just overwrites the first.
This might belong to the same family as the fabricated CALLS edges filed for Rust (#2053) and Go (#1906, #1909, #1927), but the mechanism looks different. There's no stdlib receiver involved here, and both symbols are project-local methods on the same type.
Activity
DeusData commentedon Sep 25, 2026
Thank you @jaganio again for the clean, dependency-free reproduction, and @DavidHLP for the exceptionally thorough investigation and candidate branch. The hardened-container reproduction, the repeat runs across worker counts and the RED/GREEN pipeline test are exactly the evidence we needed.
We've settled the direction. While checking it we found the collapse isn't Swift-specific: Java, C#, C++ and Kotlin overloads merge into one node the same way today (on Alamofire, the 12
Session.uploadoverloads become a single node carrying callees from all of them). Because a qualified name is persisted graph identity, we're adopting one signature-qualified identity for every overloading language (Java, C#, C++, Kotlin, Scala, Objective-C, Swift, …):base_qn[<tparams>](params)[cvref], applied to every callable, not only when a sibling exists, so adding an overload never renames an existing node. Thenamecolumn stays bare, so searchingworkstill finds every overload, and each language ships behind a one-time index-format rebuild. When a call still matches several overloads after label/arity selection, we'll emit an edge to each compatible candidate, tagged with the candidate count, rather than guess or drop it.Your label model becomes the Swift instance of that scheme, with two extensions we found while measuring Alamofire:
upload(_ data: Data, to:…),upload(_ fileURL: URL, to:…)andupload(_ stream: InputStream, to:…)share the selectorupload(_:to:…), so labels alone still merge them. The Swift form becomeswork(flag:Bool)/work(name:String), andwork()for zero-argument callables so the rule is uniform.The shared plumbing (the signature builder, base-QN-aware lookups and splitters, no language enabled yet, byte-identical graphs) is up as #2342. Languages will then be enabled one PR at a time. We'd love for you to carry the Swift PR on top of that, with your helper and regression test as its core. We'll ping you here as soon as the plumbing is on main. Thank you again for such careful work!
test(swift): cover parallel overload candidate edges
fix(swift): unify incompatible-overload fallback across resolver paths
feat(identity): callable identity plumbing, no language enabled (#2061)
feat(identity): callable identity plumbing, no language enabled (#2061)
fix(swift): match multiple trailing closure labels
fix(swift): match multiple trailing closure labels
Merge pull request #2342 from DeusData/feat/callable-identity-plumbing
DavidHLP commentedon Oct 5, 2026
I removed the original investigation while consolidating the progress comments. I'm restoring it here in full.
This was first posted on 2026-09-14 (original comment 5662955218). The findings and results below belong to that checkpoint. The current Swift follow-up and review are in PR #2436; the original snapshot remains available.
Exploration update (PR intentionally deferred)
I am posting the investigation and reproducible evidence here first. I am not opening an upstream pull request yet; the candidate branch on my fork is only investigation material at this point.
1. Scope and environment
DeusData/codebase-memory-mcpmainat0b51555402aa6aea9e5fd93d9ed53136dec4c6e8test-infrastructure/Dockerfile(Ubuntu 24.04/Noble)--security-opt no-new-privileges,--cap-drop ALL, PID limit, and an isolated writable tmpfs for build/cache dataThis issue is reported on macOS Apple Silicon, but this is not a claim of native macOS validation. Docker shares the host kernel and cannot emulate macOS. The defect is in Swift symbol identity and graph resolution, so the deterministic semantic reproduction is still useful on Linux; native macOS CI or real macOS hardware remains the platform-specific follow-up.
2. Minimal fixture
The reproduction uses the same three dependency-free Swift files described in this issue:
Sources/Sink.swiftSources/Service.swiftSources/Caller.swiftThe important invariant is that
work(flag:)callstarget(), whilework(name:)does not, andonlyCallsOverloadB()calls only the latter.3. Reproduction procedure
Inside the isolated container I built the production binary from the baseline commit and indexed the fixture in
fastmode. The equivalent requests were:The index status reported
parse_partial_count=0, so this is not a grammar parse failure.4. Baseline result: the issue reproduces
The unmodified baseline produced the same semantic failure reported here:
The only returned
worknode retains the line range ofwork(name:), but it has the outgoingCALLSedges from the other overload as well. BecauseonlyCallsOverloadB()calls that merged node, the traversal reachestarget()through an edge that cannot exist in the source program.This gives a compact red assertion: one
worknode, the wrong merged degree/line identity, and a fabricated inbound caller.5. Root-cause analysis
The failure is caused by losing Swift argument-label identity at more than one extraction boundary:
work). Both declarations onServicetherefore addressed the same graph key.work(flag:)fromwork(name:).trace_paththen correctly traversed the already-corrupted graph, which made the symptom look like a fabricated caller.The shared semantic rule needed by the graph is therefore:
6. Candidate solution explored locally
The candidate change is on my fork at commit
0bf62ed, branchfix/issue-2061. It is not an upstream PR yet.The change applies one canonical Swift callable-name rule across the affected layers:
cbm_swift_callable_namehelper ininternal/cbm/helpers.c/helpers.h.external_name, falling back to the parameter name) when creating Swift definition qualified names._.CBMCall.callee_namefor source-facing data and adds a canonicalresolution_namefor lookup.resolution_namewhen synthetic calls are copied across the LSP arena.CALLSedges, and inbound trace rows.This is intended as a root-cause fix rather than a
trace_path-specific filter: definitions, calls, resolution, and the regression contract all use the same overload identity.7. Candidate green observations already obtained
Before pausing this work, the candidate was checked with:
-Werrorflags: passed.CBM_WORKERS=1and three underCBM_WORKERS=4.nodes=18,edges=27;search_graphreturned exactlywork(flag:)andwork(name:); onlywork(flag:)had the edge totarget(); inboundtrace_pathreturned exactlywork(flag:).extractionpluspipelinesuites:621 passed, including the new regression test.The remaining pre-submission gates are deliberately not being presented as green here: the full
scripts/test.shrun andscripts/lint.sh --cistill need to be completed, and native macOS validation has not been performed.8. Request for maintainer review
Please confirm whether this identity model matches the project's intended Swift qualified-name semantics, especially the representation of unlabeled arguments and any edge cases involving default argument labels. Once that direction is confirmed, I can resume the full gate run and open a focused PR with the complete red/green evidence.