Noncopyable deinit is not called when compiling with -O

I'm having a weird issue that looks like a compiler bug. When my noncopyable struct is consumed under some circumstances, its deinit is never called.

I've created a minimal reproduction case :

import Foundation

struct SpinnerHandle: ~Copyable {
    init() {
        print("* Showing spinner *")
    }

    deinit {
        print("* Dismissing spinner *")
    }

    consuming func dismiss() {
        print("SpinnerHandle.dismiss()")
        // why is deinit() not called ???
    }
}

final class SpinnerHandleBoxed {
    private var handle: SpinnerHandle?

    private init(handle: consuming SpinnerHandle) {
        self.handle = consume handle
    }

    static func displayBoxed() -> SpinnerHandleBoxed {
        SpinnerHandleBoxed(handle: SpinnerHandle())
    }

    func dismiss() {
        if let handle = self.handle.take() {
            handle.dismiss()
        } else {
            print("Spinner was already dismissed")
        }
    }
}

func testHandle() {
    print("\nTesting handle\n")
    let handle = SpinnerHandle()
    // some time later...
    handle.dismiss()
}

func testBoxed() {
    print("\nTesting boxed\n")
    let boxed = SpinnerHandleBoxed.displayBoxed()
    // some time later...
    boxed.dismiss()
    boxed.dismiss()
}

testHandle()
testBoxed()

The testHandle() case works as expected when compiled with -Onone and -O. The testBoxed() case works as expected only with -Onone. When compiling with -O, the deinit is seemingly never called.

You can run the complete case with this script :

#!/bin/zsh
swiftc -v

echo "\nCompiling without optimisations"
swiftc -Onone main.swift -o main_onone
./main_onone

echo "\nCompiling with optimisations"
swiftc -O main.swift -o main_o
./main_o

echo "\n ISSUE: there no '* Dismissing spinner *' below 'Testing boxed' when using -O"
Output on my machine
Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: x86_64-apple-macosx15.0

Compiling without optimisations

Testing handle

* Showing spinner *
SpinnerHandle.dismiss()
* Dismissing spinner *

Testing boxed

* Showing spinner *
SpinnerHandle.dismiss()
* Dismissing spinner *
Spinner was already dismissed

Compiling with optimisations

Testing handle

* Showing spinner *
SpinnerHandle.dismiss()
* Dismissing spinner *

Testing boxed

* Showing spinner *
SpinnerHandle.dismiss()
Spinner was already dismissed

 ISSUE: there no '* Dismissing spinner *' below 'Testing boxed' when using -O

Am I doing something incorrect in the code or is this behavior not expected?

2 Likes

Maybe a question for @Joe_Groff ? :slight_smile:

1 Like

+1 — seeing this too. I simplified your example a little:

struct NonCopyable: ~Copyable {
    
    init() {
        print("Non-copyable initialized")
    }

    deinit {
        print("Non-copyable deinitialized")
    }
    
}

final class BoxedNonCopyable {
    
    var value: NonCopyable? = .init()
    
    deinit {
        print("Box deinitialized")
    }
    
    func dismiss() {
        _ = value.take()
    }
    
}

autoreleasepool {
    let boxed = BoxedNonCopyable()
    
    boxed.dismiss()
}
Non-copyable initialized
Box deinitialized
// "Non-copyable deinitialized" is never printed!

What's odd is that if you get rid of the explicit dismiss() the non-copyable is deinitalized correctly:

autoreleasepool {
    let boxed = BoxedNonCopyable()
    
    //boxed.dismiss()
}
Non-copyable initialized
Box deinitialized
Non-copyable deinitialized
2 Likes

Yeah, that looks like a bug.

4 Likes