-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[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
Conversation
@swift-ci Please test |
There was a problem hiding this 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 👍
There was a problem hiding this 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!
lib/IDE/Refactoring.cpp
Outdated
// - 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"contiuation" -> "continuation"
lib/IDE/Refactoring.cpp
Outdated
@@ -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 = {}) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess https://github.com/apple/swift/blob/aa3ede15389ced5e0cdc017702906d5e2a4e953d/lib/IDE/Refactoring.cpp#L6327 should also be an llvm:function_ref
then, right?
lib/IDE/Refactoring.cpp
Outdated
// Carry over the continuation name. | ||
Identifier PreviousContinuationName = Scopes.back().ContinuationName; | ||
Scopes.emplace_back(PreviousContinuationName); | ||
} | ||
for (auto DeclAndNumRefs : Decls) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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: } |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) {} | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"continuationinuation" 😆
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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 🤷♂️
There was a problem hiding this comment.
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 😆
199f845
to
3ded59e
Compare
@swift-ci Please smoke test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
3ded59e
to
1744d8e
Compare
@swift-ci Please smoke test |
There was a problem hiding this 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
1744d8e
to
54fcc90
Compare
@swift-ci Please smoke test |
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
becomes
Example 2
becomes
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
Example 2
I think both are much closer to what the developer is actually expecting.
Resolves rdar://79304583