Skip to content

6.0.3: Unrelated structs end up sharing a closure when they shouldn't #78922

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
GarthSnyder opened this issue Jan 26, 2025 · 3 comments
Open
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels

Comments

@GarthSnyder
Copy link
Contributor

Description

The code below defines a struct, ClosureStorer, that accepts a closure as an initialization argument. The class UsesStorers creates two of these structs, one with < as an argument and the other with >. The problem is that both structs end up sharing a reference to the same closure.

I suspect that the underlying issue lies somewhere in the domain of class initialization. If the line

let greater = ClosureStorer(>), lessThan = ClosureStorer(<)

is rewritten to

let greater = ClosureStorer(>)
let lessThan = ClosureStorer(<)

then the problem does not occur.

Other cases where the problem doesn't occur:

  • If the ClosureStorers are initialized at the top level rather than inside a class definition.
  • If the arguments to ClosureStorer() are the names of freestanding functions rather than operators.

Reproduction

Run the following code:

struct ClosureStorer {
    let op: (Int, Int) -> Bool
    init(_ op: @escaping (Int, Int) -> Bool) {
        self.op = op
    }
    func compare(_ a: Int, _ b: Int) -> Bool {
        return op(a, b)
    }
}

class UsesStorers {
    let greater = ClosureStorer(>), lessThan = ClosureStorer(<)
    func test() {
        print(greater.compare(1, 2), lessThan.compare(1, 2))   // false false
        print(greater.compare(2, 1), lessThan.compare(2, 1))   // true true
    }
}

let user = UsesStorers()
user.test()

As indicated in the comments, the two ClosureStorer structs return (or should return, if the issue is being demonstrated correctly) the same comparison results.

Xcode shows ClosureStorer.op as having the same address for both copies of the struct. The debug output labels this "implicit closure #1".

Expected behavior

The two ClosureStorer structs are independent and should each return results that conform to the comparison operator they're initialized with. The correct output is "false true / true false", demonstrating that the closures return different results.

Environment

I'm seeing this problem on Xcode 16.2 (16C5032a), which I believe is Swift 6.0.3.

Additional information

No response

@GarthSnyder GarthSnyder added bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels labels Jan 26, 2025
@GarthSnyder
Copy link
Contributor Author

I see in Godbolt that only one lea instruction is present in the initialization code for UsesStorers and that the result is copied to both structs:

output.UsesStorers.__allocating_init() -> output.UsesStorers:
        push    rax
        mov     rdi, r13
        mov     esi, 48
        mov     edx, 7
        call    swift_allocObject@PLT
        lea     rcx, [rip + (implicit closure #1 (Swift.Int, Swift.Int) -> Swift.Bool in variable initialization expression of output.UsesStorers.greaterThan : output.ClosureStorer)]
        mov     qword ptr [rax + 16], rcx
        mov     qword ptr [rax + 24], 0
        mov     qword ptr [rax + 32], rcx
        mov     qword ptr [rax + 40], 0
        pop     rcx
        ret

@jamieQ
Copy link
Contributor

jamieQ commented Jan 26, 2025

this looks, at least superficially, quite similar to this issue with let tuple destructuring within structs: #68915.

here's a slight reduction:

class C {
  let a = { 1 }, b = { 2 }

  func test() {
    assert(a() != b()) // 1 != 1
  }
}

the problem appears to affect all stored property declarations – within structs, classes, actors, static properties in enums, etc – and also is seemingly independent of let or var use.

edit: also, seems this bug may have existed 'forever'. compiler explorer suggests the issue goes back to at least Swift 3.1.1

@GarthSnyder
Copy link
Contributor Author

this looks, at least superficially, quite similar to this issue with let tuple destructuring within structs: #68915.

It looks like #68915 and companion bug #68916 were closed in the wake of #68930, which was merged back in 2023. However, #68930 isn't a fix but a stopgap to detect cases known not to work and make them generate an error.

The reproduction scenarios above don't generate compilation errors, so if they are in fact an effect of the same underlying bug, they're likely slipping through the filter put in place by #68930.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels
Projects
None yet
Development

No branches or pull requests

2 participants