DEV Community

Art Oz
Art Oz

Posted on

I Asked Claude to Create a Memory Leak in a Task and It Failed

Last week, I asked Claude to create a memory leak in Swift's Task API, for an amusement purposes. It gave me code and claimed the Task captures self strongly, creating a retain cycle. It suggested using [weak self].

I knew something was off. Tasks don't create retain cycles by default, that's literally their design advantage.

I diasgreed with him and he started to explain why I am right. The problem is that other than trusting his words I did not have any other way to prove I am right.

The Investigation

It turns out the Swift compiler creates something called SIL (Swift Intermediate Language) - an intermediate representation that shows exactly what the compiler does with your code before turning it into machine code. It's like looking at the compiler's notes.

bash
swiftc -emit-sil MemoryLeak.swift | xcrun swift-demangle > output.sil

 %14 = function_ref @(extension in Swift):Swift.Task< where B == Swift.Never>.init(priority: Swift.TaskPriority?, operation: __owned @isolated(any) () async -> A) -> Swift.Task<A, Swift.Never> : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Sendable, τ_0_1 == Never> (@in Optional<TaskPriority>, @sil_sending @owned @isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <τ_0_0>, @thin Task<τ_0_0, Never>.Type) -> @owned Task<τ_0_0, Never> // user: %15
  %15 = apply %14<(), Never>(%3, %13, %2) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Sendable, τ_0_1 == Never> (@in Optional<TaskPriority>, @sil_sending @owned @isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <τ_0_0>, @thin Task<τ_0_0, Never>.Type) -> @owned Task<τ_0_0, Never> // user: %17
  dealloc_stack %3 : $*Optional<TaskPriority>     // id: %16
  release_value %15 : $Task<(), Never>            // id: %17
  %18 = tuple ()                                  // user: %19
  return %18 : $()                                // id: %19
} // end sil function 'MemoryLeak.LeakyViewController.startLeakyTask() -> ()'
Enter fullscreen mode Exit fullscreen mode

This is just a small part of it, the whole file is about 500 lines if code. If not demangling it I could look at it infinitely.

The Proof

Lines that told the whole story:
release_value %15 : $Task<(), Never>

That release_value instruction proves everything:

  • Task created
  • Task handle released immediately
  • Task NOT stored
  • No cycle

You can read about it here: [(https://apple-swift.readthedocs.io/en/latest/SIL.html#release-value)]

The Reality

Task → ViewController 

(one-way)
ViewController → Task? NO

No cycle = No leak
Enter fullscreen mode Exit fullscreen mode

The deinit WILL be called. There is no memory leak.

What I Learned

Sometimes you need to go deeper than the documentation. The best way to truly understand how something works is to look at what the compiler actually does, not what we assume it does.

Trust, but verify. Especially with AI.

Top comments (0)