Skip to content
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

@autoclosure gets confused when passing a throwing closure as a variable #59578

Open
CharlesJS opened this issue Jun 19, 2022 · 6 comments
Open
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.

Comments

@CharlesJS
Copy link

Describe the bug

Suppose you have the following code:

func foo(_: @autoclosure () throws -> Void) {}

let bar: () throws -> Void = {}

let baz: () -> Void = {
    foo(bar)
}

This results in a compiler error on foo(bar): Add () to forward @autoclosure parameter.

Trying the autofix results in foo(bar()), which yields the compiler error Call can throw but is not marked with 'try'.

Reasonable enough, so let's add a try and make the line foo(try bar()). Now it fails on the previous line with: Invalid conversion of type () throws -> Void' to non-throwing function type '() -> Void'.

Interestingly enough, the last line works fine if the foo call is at the top level of the source file. It is only when it is embedded within a non-throwing closure that this error occurs.

This can be worked around by casting foo to a closure without @autoclosure in it:

func foo(_: @autoclosure () throws -> Void) {}

let bar: () throws -> Void = {}

let baz: () -> Void = {
    let _foo = foo as (() throws -> Void) -> Void
    _foo(bar) // this works fine 🤷
}

To Reproduce
Steps to reproduce the behavior:
Try to compile the code snippets in the description.
...

Expected behavior
Since foo takes a throwing closure, but does not itself throw (presumably it catches any errors and handles them somehow), a call to foo should not be interpreted as throwing.

Environment (please complete the following information):

  • OS: macOS 12.4 (21F79)
  • Xcode Version/Tag/Branch: 14.0 beta (14A5228q)
 $ swift --version
swift-driver version: 1.55.1 Apple Swift version 5.7 (swiftlang-5.7.0.113.202 clang-1400.0.16.2)
Target: x86_64-apple-macosx12.0
@CharlesJS CharlesJS added the bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. label Jun 19, 2022
@LucianoPAlmeida
Copy link
Contributor

The behavior is correct I think, foo(try bar()) should fail inside closure, because there is a try but closure let baz: () -> Void = { is not marked as throws. I think the confusion in this case is in how auto closures work and that autoclosure parameter should not accept a function type as argument e.g. @autoclosure () throws -> Void does not mean it can accept values of type () throws -> Void that means it accepts values of type Void and trying to pass a function should ideally not correct.

One thing is that forcing a conversion to erase the auto closure attribute works let _foo = foo as (() throws -> Void) -> Void, but IMO this looks like a hack and not sure this even should be allowed... We couldn't change that because of source compatibility but just for curiosity is this behavior intentional?
cc @xedin

@CharlesJS
Copy link
Author

CharlesJS commented Jun 21, 2022

@autoclosure is just syntactic sugar for passing a closure without the braces. It shouldn't change the semantics of passing a closure, and it definitely shouldn't make it literally impossible to pass certain types of closures that fit the signature.

See the docs:

This syntactic convenience lets you omit braces around a function’s parameter by writing a normal expression instead of an explicit closure.

It's also unclear as to what behavior will actually happen here. If I call foo from my example above from a throwing context, where will the error be thrown?

do {
    foo(try bar())
} catch {
    // Do we ever get here?
}

If an error is thrown inside bar, will we get to the catch block, or would it be handled inside foo? I would expect the latter, since foo is defined as non-throwing. But if the compiler is forcing me to make the enclosing function throwing, maybe it will? It's pretty ambiguous.

Incidentally, if you do it at the top level of the file, instead of inside a baz function, it works fine:

struct MyError: Error {}

func foo(_: @autoclosure () throws -> Void) {
  do {
    try bar()
  } catch {
    print("we caught an error in foo")
  }
}

let bar: () throws -> Void = { throw MyError() }

foo(try bar()) // this works fine, believe it or not

print("got here")

outputs:

we caught an error in foo
got here

This is all particularly obnoxious because the XCTAssert family of functions all take @autoclosures while not throwing, making it pretty obnoxious to write test helpers that pass closures through, or make workarounds for the fact that they don't take async closures.

@LucianoPAlmeida
Copy link
Contributor

@autoclosure is just syntactic sugar for passing a closure without the braces. It shouldn't change the semantics of passing a closure, and it definitely shouldn't make it literally impossible to pass certain types of closures that fit the signature.

Not sure I'm not following ... what I mean is that an autoclosure parameter func foo(_: @autoclosure () throws -> Int) should not take a closure of type () throws -> Int because the auto closure makes the expected parameter to be an Int value.

@CharlesJS
Copy link
Author

Unfortunately, the XCTest functions all do this, so if you disallowed this, you'd break pretty much everybody's tests.

@AnthonyLatsis
Copy link
Collaborator

AnthonyLatsis commented Jun 22, 2022

In general, an @autoclosure parameter does not accept values of its function type, because it implicitly wraps an argument in a closure of that type. The bug has to do with the try in foo(try bar()), which desugars to foo({ try bar() }) and is supposed to work. This appears to be a long-standing issue.

@AnthonyLatsis
Copy link
Collaborator

Duplicate of #43104

@AnthonyLatsis AnthonyLatsis marked this as a duplicate of #43104 Jun 22, 2022
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.
Projects
None yet
Development

No branches or pull requests

3 participants