How Swift decides a closure as escaping?

I understand what escaping & non-escaping does. But surely do not understand how Swift actually decides what to make @escaping and what not to.

1. This gets compiled

func someFunc( _ clsr: ()->Void) {
  let clsr2 = {
    clsr()
  }
}

2. This Doesn't

func someFunc( _ clsr: ()->Void) {
  let clsr2 = {
    clsr()
  }

  clsr2()
}

3. This also doesn't, although 1 works fine

func someFunc( _ clsr: ()->Void) {
  let clsr2 = {
    clsr()
  }

  let clsr3 = {
    clsr2()
  }
}

4. This works fine

func someFunc( _ clsr: ()->Void) {
  let clsr2 = {
    clsr()
  }

  let clsr3 = {
    clsr()
  }
}

5. This works fine

func someFunc( _ clsr: ()->Void) {
  let clsr2 = {
    clsr()
  }

  func clsr3() {
    clsr2()
  }
}

func someFunc2( _ clsr: ()->Void) {
  clsr()
}

6. This don't

func someFunc( _ clsr: ()->Void) {
  let clsr2 = {
    clsr()
  }

  func clsr3() {
    clsr2()
  }

  someFunc2(clsr3)
}

func someFunc2( _ clsr: ()->Void) {
  clsr()
}

swift version: swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4) Target: arm64-apple-macosx14.0

In those cases it compiles - it’s only possible because you are not actually using escaped closure, which is useless as once you try to actually use it - it won’t compile. Swift could have prohibited it in all instances to make it less “promising”, but it is what it is. The rule is basically “don’t escape non escaping closure, and if you managed to create an escaping closure from non escaping - don’t actually use it (it won’t be allowed)”.

1 Like

Thanks for your reply. I will add few more example to contradict what you are saying.

According to you, the below one won't compile and that's correct.

func someFunc( _ clsr: ()->Void) {
  let clsr2 = {
    clsr()
  }

  let clrs3 = {
    clsr2()
  }

  someFunc2(clsr2)
}

func someFunc2( _ clsr: ()->Void) {
  clsr()
}

But this compiles fine, contradicting to you earlier statement.

func someFunc( _ clsr: ()->Void) {
  let clsr2 = {
    clsr()
  }

  someFunc2(clsr2)
}

func someFunc2( _ clsr: ()->Void) {
  clsr()
} 
1 Like

I believe it’s because, in the general case, clsr3 can cause clsr2 to escape, but someFunc2 can’t (without @escaping).

There's indeed some sloppiness in allowing this:

func foo(_ closure: () -> Void) {
    func someFunc(_ closure: () -> Void) { closure() }
    let f = { closure() }
    someFunc(f)
}

and prohibiting this:

func bar(_ closure: () -> Void) {
    let f = { closure() }
    f()
}

as in both cases the closure is not actually "escaped"; in the ideal world the "permissive" approach should have resulted in both fragmented being allowed whilst the "prohibitive" approach should have prohibited both fragments, and what we have here is an inconsistency and not ideal. Is that a big deal? No.


Edit: Interestingly changing let to var in this app affects it compile-ability:

func foo(_ closure: () -> Void) {
    let f = { closure() } // âś…
    var g = { closure() } // 🛑
}

I think that sometimes the compiler has enough information to know a closure is escaping:

func someFunc2( _ clsr: ()->Void) {
   Task { clsr() } 
}

Here it knows that the closure is escaping and that it will never not be escaping

So it requires @escaping in the sig

According to my own experience, there is no "explicit rule" about escaping checking.

Correct me if I'm wrong, I think they must be using some heuristics in the compiler. And I believe that's part of the reason why withoutActuallyEscaping is provided.

A variation of your case 2:

func someFunc( _ clsr: ()->Void) {
  withoutActuallyEscaping(clsr) { clsr in
    let clsr2 = {
      clsr()
    }

    clsr2()
  }
}

FYI, in some cases, if you change a local closure value to a local function, things will compile.

Another variation of your case 2:

func someFunc( _ clsr: ()->Void) {
  func clsr2() {
    clsr()
  }

  clsr2()
}
1 Like