Skip to content

[Async Refactoring] Wrap code in a continuation if conversion doesn't yield reasonable results #38193

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

Merged
merged 2 commits into from
Jul 5, 2021

Conversation

ahoppen
Copy link
Member

@ahoppen ahoppen commented Jul 1, 2021

If we are requested to convert a function to async, but the call in the function’s body that eventually calls the completion handler doesn’t have an async alternative, we are currently copying the call as-is, replacing any calls to the completion handler by placeholders.

Example 1

func testDispatch(completionHandler: @escaping (Int) -> Void) {
  DispatchQueue.global.async {
     completionHandler(longSyncFunc())
  }
}

becomes

func testDispatch() async -> Int  {
  DispatchQueue.global.async {
     <#completionHandler#>(longSyncFunc())
  }
}

Example 2

func testUrlSession(completionHandler: @escaping (Data) -> Void) {
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
    completion(data!)
  }
  task.resume()
}

becomes

func testUrlSession() async -> Data {
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
    <#completion#>(data!)
  }
  task.resume()
}

Both of these are better modelled using continuations. Thus, if we find an expression that contains a call to the completion handler and can’t be hoisted to an await statement, we are wrapping the rest of the current scope in a withChecked(Throwing)Continuation, producing the following results:

Example 1

func testDispatch() async -> Int {
  return await withCheckedContinuation { (continuation: CheckedContinuation<Int, Never>) in
    DispatchQueue.global.async {
      continuation.resume(returning: longSyncFunc())
    }
  }
}

Example 2

func testDataTask() async -> Int?
  return await withCheckedContinuation { (continuation: CheckedContinuation<Data, Never>) in
    let task = URLSession.shared.dataTask { data, response, error in
      continuation.resume(returning: data!)
    }
    task.resume()
  }
}

I think both are much closer to what the developer is actually expecting.

Resolves rdar://79304583

@ahoppen ahoppen requested review from bnbarham and hamishknight July 1, 2021 14:00
@ahoppen
Copy link
Member Author

ahoppen commented Jul 1, 2021

@swift-ci Please test

Copy link
Contributor

@ktoso ktoso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed this one quite a bit with Ben, looks good!

Thankfully we have the checked continuations so any abuse will crash in expected ways which is fine... Agree that it's more what users are expecting from such refactoring 👍

Copy link
Contributor

@bnbarham bnbarham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually ended up not too bad at all. Thanks Alex!

// - the current expression is nested (we can't start hoisting in the
// middle of an expression)
// - the current scope is wrapped in a continuation (we can't have await
// calls in the contiuation block)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"contiuation" -> "continuation"

@@ -6331,6 +6463,15 @@ class AsyncConverter : private SourceEntityWalker {
return false;
}

/// Insert custom text at the given \p Loc that shouldn't replace any existing
/// source code.
bool insertCustom(SourceLoc Loc, std::function<void()> Custom = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use llvm::function_ref since we're just calling it here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Carry over the continuation name.
Identifier PreviousContinuationName = Scopes.back().ContinuationName;
Scopes.emplace_back(PreviousContinuationName);
}
for (auto DeclAndNumRefs : Decls) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you're here would you mind fixing this up to just "D" or something? I assume it was a map of Decl -> number of references at some point?

@@ -143,13 +143,14 @@ func asyncUnhandledCompletion(_ completion: (String) -> Void) {
}
// ASYNC-UNHANDLED: func asyncUnhandledCompletion() async -> String {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename since it is handled now?

// ASYNC-UNHANDLED-NEXT: }
// ASYNC-UNHANDLED-NEXT: if !success {
// ASYNC-UNHANDLED-NEXT: continuation.resume(returning: "bad")
// ASYNC-UNHANDLED-NEXT: }
// ASYNC-UNHANDLED-NEXT: }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs one more } here

// ASYNC-UNHANDLED-NEXT: if !success {
// ASYNC-UNHANDLED-NEXT: {{^}} return "bad"{{$}}
// ASYNC-UNHANDLED-NEXT: }
// ASYNC-UNHANDLED-NEXT: return await withCheckedContinuation { (continuation: CheckedContinuation<String, Never>) in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the type ever required in the closure? If not I'd prefer just return await withCheckedContinuation { continuation in

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent question. I needed it when I forgot to write return in front of it but it’s not needed anymore. Thanks!

func withoutAsyncAlternativeBecauseOfReturnValue(completionHandler: (Int) -> Void) -> Bool {}
func withoutAsyncAlternativeThrowing(closure: (Int?, Error?) -> Void) {}
func asyncVoidWithoutAlternative(completionHandler2: () -> Void) {}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need some tests for a completion handler with a Result argument and then also handling to use continuation(with: result) as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, the entire Result case wasn’t covered by the implementation yet. I added two test cases now.

// THROWING-CONTINUATION-NEXT: }
// THROWING-CONTINUATION-NEXT: }

// We can't relay both the result and the error through the continuationinuation. Converting the following results in a compiler error complaining that theError (of type Error?) can't be passed to `continuation.resume(throwing)`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"continuationinuation" 😆

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂

// THROWING-CONTINUATION-RELAYING-ERROR-AND-RESULT: func testThrowingContinuationRelayingErrorAndResult() async throws -> Int {
// THROWING-CONTINUATION-RELAYING-ERROR-AND-RESULT-NEXT: return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Int, Error>) in
// THROWING-CONTINUATION-RELAYING-ERROR-AND-RESULT-NEXT: withoutAsyncAlternativeThrowing { (theValue, theError) in
// THROWING-CONTINUATION-RELAYING-ERROR-AND-RESULT-NEXT: continuation.resume(throwing: theError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to check for this case specifically and add our own handling? Ie. convert this into:

if let err = theError {
  continuation.resume(throwing: theError)
} else {
  continuation.resume(returning: theValue) // or (a, b, c, d) if multiple
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing this with you, I'm happy to just leave the ambiguous case to another PR. We have a similar problem with the regular hoisting where we end up with throw <#err#>. This is better than what we have at the moment anyway.

func testDataTask(_ completion: @escaping (Int?) -> Void) {
let dataTask = URLSession.shared.dataTask { (data, error) in
guard let data = data else {
return // Yes, there is a bug about not calling completion here, but that's copied from the Twitter link above
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least we have checked continuations I guess 😨. Ends up being a log, which is better than nothing...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, it was broken before. There’s not much more we can do 🤷‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, arguably the log is better than what it was 😆

@ahoppen ahoppen force-pushed the pr/use-continuation branch from 199f845 to 3ded59e Compare July 2, 2021 09:29
@ahoppen
Copy link
Member Author

ahoppen commented Jul 2, 2021

@swift-ci Please smoke test

Copy link
Contributor

@bnbarham bnbarham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ahoppen ahoppen force-pushed the pr/use-continuation branch from 3ded59e to 1744d8e Compare July 2, 2021 09:44
@ahoppen
Copy link
Member Author

ahoppen commented Jul 2, 2021

@swift-ci Please smoke test

Copy link
Contributor

@hamishknight hamishknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

… yield reasonable results

If we are requested to convert a function to async, but the call in the function’s body that eventually calls the completion handler doesn’t have an async alternative, we are currently copying the call as-is, replacing any calls to the completion handler by placeholders.

For example,
```swift
func testDispatch(completionHandler: @escaping (Int) -> Void) {
  DispatchQueue.global.async {
     completionHandler(longSyncFunc())
  }
}
```
becomes
```swift
func testDispatch() async -> Int  {
  DispatchQueue.global.async {
     <#completionHandler#>(longSyncFunc())
  }
}
```

and

```swift
func testUrlSession(completionHandler: @escaping (Data) -> Void) {
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
    completion(data!)
  }
  task.resume()
}
```
becomes
```swift
func testUrlSession() async -> Data {
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
    <#completion#>(data!)
  }
  task.resume()
}
```

Both of these are better modelled using continuations. Thus, if we find an expression that contains a call to the completion handler and can’t be hoisted to an await statement, we are wrapping the rest of the current scope in a `withChecked(Throwing)Continuation`, producing the following results:

```swift
func testDispatch() async -> Int {
  return await withCheckedContinuation { (continuation: CheckedContinuation<Int, Never>) in
    DispatchQueue.global.async {
      continuation.resume(returning: syncComputation())
    }
  }
}
```

and

```swift
func testDataTask() async -> Int?
  return await withCheckedContinuation { (continuation: CheckedContinuation<Data, Never>) in
    let task = URLSession.shared.dataTask { data, response, error in
      continuation.resume(returning: data!)
    }
    task.resume()
  }
}
```

I think both are much closer to what the developer is actually expecting.

Resolves rdar://79304583
@ahoppen ahoppen force-pushed the pr/use-continuation branch from 1744d8e to 54fcc90 Compare July 2, 2021 13:48
@ahoppen
Copy link
Member Author

ahoppen commented Jul 2, 2021

@swift-ci Please smoke test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants