Skip to content

fix(swift): read the binding that is in scope at the call - #2947

Merged
DeusData merged 2 commits into
mainfrom
fix/swift-in-scope-binding
Nov 10, 2026
Merged

DeusData merged 2 commits into
mainfrom
fix/swift-in-scope-binding

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Refs #2436

This is the first of the follow-ups we promised when #2436 was merged. Thank you, @DavidHLP, for the shadowing example in that thread. It was exactly the case this change needed, and your trace to the binding lookup was spot on.

What was wrong

A Swift receiver's type came from every binding with the same name in the enclosing function, wherever it was declared. When an inner binding's type could not be inferred, the receiver counted as unknown even at a call where only the outer, declared binding is in scope, and the call fell back to the label match:

let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
if flag {
    let semaphore = identity(DispatchSemaphore(value: 0))
    semaphore.signal()
}
semaphore.wait()   // had an edge to an unrelated project wait()

This came from our own commit 8372793 in #2436. Treating "one binding known, one unknown" as a conflict would fix the example but drop correct edges on Alamofire's var urlRequest = try urlRequest.asURLRequest() idiom, so this change picks the binding that is actually in scope at the call instead.

What changed

  • Each Swift binding (a parameter or a let / var) records where it is in scope: from the end of its declaration to the end of its block. A parameter's block is its function.
  • Only the bindings in scope at the call count, and of those the innermost block's. A local shadows a same-name local of an enclosing block, or a parameter. var request = try request.make() reads the parameter on its right-hand side and the new variable after it.
  • Bindings of one block that disagree still select nothing, as before. Fields and module-level bindings are unchanged.

Test

pipeline_swift_in_scope_binding_issue2436 (sequential and parallel) covers the example from #2436 (no edge to Decoy.wait()), the shadowed-parameter idiom with an any parameter (the new variable's call keeps its edge), and two sibling blocks that each type their own store. It fails without the change.

Measured

main 0d835c5 vs this branch; Alamofire bda9ed57, swift-nio eabcc14f, helm 9b88efd6; every run indexed from scratch.

Alamofire main this branch swift-nio main this branch
CALLS edges 8,032 8,029 29,711 29,739
caller/target pairs removed / added 4 / 1 16 / 44
resolved call sites 5,637 5,634 27,464 27,492
single-candidate edges 4,117 4,113 22,776 22,804
TESTS edges 0 0 64 64

We reviewed every changed pair, not a sample, since the deltas are small:

  • Alamofire: the 4 removed edges are the right-hand-side calls of the shadowing idiom (urlRequest.asURLRequest() three times, url.asURL() once) on an any URLRequestConvertible / any URLConvertible parameter. main label-matched them to one arbitrary conformer (Session's private ParameterlessRequestConvertible, String.asURL). The calls on the new variable, such as urlRequest.headers.update(...), keep their edges. The 1 added edge (configuration.headers inside the closure that initializes a same-name configuration) is right.
  • swift-nio: of the 16 removed pairs, 12 were wrong (11 channelFuture.whenSuccess on EventLoopFuture.Isolated instead of EventLoopFuture, and upgraders.map). The other 4 were right only because a same-name binding from another closure or test method happened to have the same type. Of the 44 added pairs, 32 are right: the 11 whenSuccess calls now on EventLoopFuture, socket.close() after let socket = try Socket(socket: socket), and ConditionLock, ByteBuffer and EventLoop calls. The other 12 come from fallbacks this change does not touch: the label match for a constructed type the project does not declare (DispatchSemaphore(...) then wait(), as noted in fix(swift): preserve declaration identity and compatible call edges #2436), for closure parameters and pattern bindings that are not recorded as bindings, and through property chains.
  • Unchanged: helm is byte-identical; there are no call edges between the NIOFS and _NIOFileSystem copies; the generated k=60/120 repositories keep 3 call edges per caller (5 for the constructor-typed variant); repeated runs are byte-identical; and sequential, default and single-worker indexing produce the same call edges.

Not in this change: an any P / some P parameter type still names no type, so a call on such a parameter gets no typed edge.

A receiver's type came from every same-name binding of the enclosing
function, wherever in the function it was declared. When an inner
binding's type could not be inferred, the receiver counted as unknown
even where only an outer, declared binding was in scope, and the call
took the label match. In

    let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
    if flag {
        let semaphore = identity(DispatchSemaphore(value: 0))
        semaphore.signal()
    }
    semaphore.wait()

the outer wait() got an edge to an unrelated project wait() (#2436).

Each Swift binding now records where it is in scope: from the end of
its declaration to the end of its block, where a parameter's block is
its function. Only the bindings in scope at the call count, and of
those the innermost block's, so a local shadows a same-name local of an
enclosing block or a parameter. `var request = try request.make()`
reads the parameter on its right-hand side and the new variable after
it. Bindings of the same block that disagree still select nothing, and
fields and module-level bindings are unchanged.

pipeline_swift_in_scope_binding_issue2436 (sequential and parallel)
covers the example, the shadowed-parameter idiom of Alamofire's
encoders and two sibling blocks; it fails without the change.

Signed-off-by: Martin Vogel <[email protected]>
@DeusData
DeusData merged commit 50079ed into main Nov 10, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant