Async, await and @autoclosure

The following does not work in swift:nightly-main-focal:

struct Baz { }
func foo() async -> Baz { Baz() }
func bar() async -> Baz { Baz() }
func | (
  lhs: @autoclosure () async -> Baz,
  rhs: @autoclosure () async -> Baz
) async -> Baz {
  Baz()
}
/// Error: call is 'async' in an autoclosure argument that is not marked with 'await'
let _ = await foo() | bar()

Adjusting the last line to let _ = await foo() | await bar() creates a different issue: // Error: 'await' cannot appear to the right of a non-assignment operator, and the original error is still raised for foo().

There is some discussion of async, await, and @autoclosure here, but it doesn't mention operators and it isn't 100% clear on the how the compiler would interpret await computeArgumentLater(await getIntSlowly()) (my reading of this is that there would be a suspension point both prior to and in the body of computeArgumentLater).

Overall, I think the right move is to only apply autoclosure semantics when an argument is not labeled await. This makes it explicit that await computeArgumentLater(await getIntSlowly()) would introduce a suspension point prior to the argument and the computed value of getIntSlowly would be promoted to a new async autoclosure before being passed to computeArgumentLater. Operators would also just work as expected (await foo() | bar).
As an added benefit, this would allow us to lift the restriction on functions with async autoclosure arguments needing to be async. It seems perfectly valid if computeArgumentLater detached a task where it called getIntSlowly and returned. In this case, labelling the statement with await isn't even semantically helpful since there would be no suspension points in the resulting closure (referring to let closure = { … } from the proposal).

Finally, if you are wondering "why is George even worried about this", I'm prototyping a framework for doing shell-script-like tasks in Swift. In this framework, I want a statement like cat("Foo.txt") to effectively run the command $ cat Foo.txt to completion, but I also want cat("Foo.txt") | sed("s/Bar/Baz/") to do the right thing (execute cat and sed simultaneously, piping input from one to the other). In my implementation I hoped to achieve this by making | take async autoclosure arguments, but then I ran into the aforementioned problem. If you are interested in looking at the code, I have a work in progress here:
https://github.com/GeorgeLyon/Swish/blob/2e69e330fd58b3455ed3394de88b73dc3bda73e7/Sources/Sample/main.swift#L6-L7

1 Like

Seems like a bug.

The point of the discussion in the proposal is that only one await is ever required per statement, just like only one try is ever required per statement, and how autoclosures must work in order to observe that principle.

Thanks for the quick reply!

This is good to know, I missed this context and read it as a more principled stance. Hopefully it is just a bug like you described (I filed one here).

It's a known current limitation.

At least for ?? and || there was some progress towards unlocking it: stdlib: Add reasync variants of '&&', '||' and '??' by slavapestov · Pull Request #36614 · apple/swift · GitHub and DNM: stdlib: Add reasync variants of '&&', '||' and '??' by aschwaighofer · Pull Request #36998 · apple/swift · GitHub

Has there been any progress on this, or is this something that is likely to miss the Swift 5.5 release? It looks like the PRs you linked are about reasync... is there a PR / bug for autoclosure support?

What's the current state of this? Is there a way to

let _ = await bar() ?? baz.asyncGetProperty

that I am missing or has this yet to be resolved?

1 Like