-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Concurrency] Implement type checking for 'async let' declarations. #34573
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
Could you add test cases for multiple patterns? like async let veggies = try chopVegetables(), meat = marinateMeat() Edit: I see you have |
Perform very basic semantic analysis for their well-formedness.
Extend effects checking to ensure that each reference to a variable bound by an 'async let' is covered by an "await" expression and occurs in a suitable context.
…all. The initializer of an 'async let' is executed as a separate child task that will run concurrently with the main body of the function. Model the semantics of this operation by wrapping the initializer in an async, escaping autoclosure (representing the evaluation of the child task), and then a call to that autoclosure (to This is useful both for actor isolation checking, which needs to treat the initializer as executing in concurrent code, and also (eventually) for code generation, which needs to have that code in a closure so that it can be passed off to the task-creation functions. There are a number of issues with this implementation producing extraneous diagnostics due to this closure transformation, which will be addressed in a follow-up commit.
Customize diagnostics when an 'await' is missing in an 'async let' initializer. While here, fix the coverage checking so we also diagnose a missing 'try'.
…itializer. When an 'async let' initializer can throw, any access to one of the variables in the 'async let' can also throw, so require such accesses to be annotated with 'try'.
Sure, I've added it, plus a bunch of other tests. |
@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.
Looking great, just some small inline comments 👍
|
||
func testAsyncLetIsolation() async { | ||
async let x = self.synchronous() | ||
// expected-error @-1{{actor-isolated instance method 'synchronous()' is unsafe to reference in code that may execute concurrently}} |
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.
Hmm... this one is interesting 🤔 Took me quite a moment to stare at it and ponder if the error is right or not. I guess it is correct, the reasoning is a bit non trivial to get to that perhaps though?
Sanity checking first then:
So we're saying that:
The (async initializer) closure is permitted but not required to have async function type.
So okey, it can indeed be the synchronous()
function. That would mean we have a child task which will invoke this synchronous actor function; I guess that's where I first stumbled already -- there model wise there isn't really "synchronous actor function" because there are no "actor functions" but we only talk about "async functions being actor isolated"...?
So okey, async let = synchronous thing
is okey... Then, we also say:
The closure executes on whatever actor it is restricted to, if it is restricted, or else on an unspecified executor.
So... I interpret this wording as "the closure executes on the target we invoke"? And since this function is synchronous... it is not associated with this actor (?!) so it would execute on an unspecified executor and that explains the current diagnostic?
Is my analysis so far correct?
--
Given that... that makes is a bit weird... I guess it was a bit surprising, but it makes sense with those rules. Not sure what a better error message would be -- explaining all the "why?" maybe 🤔
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 agree that we could do better with the "why?" here, e.g., we could call out that we're in a child task.
|
||
// REQUIRES: concurrency | ||
|
||
async let x = 1 // okay |
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.
Curious: why allow non-async initializers?
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.
The work you want to run concurrently could be synchronous work. Why not?
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 didn't think that one through, sorry. I assumed (I don't know why) that concurrent code would be via async functions.
async let x = await getInt() | ||
print(x) // expected-error{{reference to async let 'x' is not marked with 'await'}} | ||
print(await x) |
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.
Compared to the proposals and other docs, this is the first I've seen await
being used (required?) on an async let
initializer. Is the await
keyword going to be required for initializers that call async functions? Are there two suspension points, at the initializer and at the subsequent read (on line 161)?
I would think the await
would only be required in a case like this:
async let y = getY()
async let x = await calculateX(y)
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 (intentionally) deviated from the proposal document, because as I was writing the tests I felt like skipping the await
was arbitrary and confusing.
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.
oh, why arbitrary to not have it? My feeling was the opposite, I thought that adding the await
reads like a suspension point, but if it's not a suspension point then the await
seems arbitrary. And as I mentioned, the two await
s also struck me as potentially confusing. I'm not arguing a position, just wanted to understand this use, and to share my perspective, for what it's worth.
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.
We should take this over to the forums. You're right that the async let
itself isn't necessarily a suspension point, so in that sense, the await
might be misleading. On the other hand, it tells you whether you have asynchronous code within the initializer, which... might?... be interesting.
Implement parsing and type checking for 'async let' declarations. 'async let' allows one to create child tasks that execute concurrently with the current task. This covers:
async let
declarations within asynchronous functionsasync let
declaration is covered byawait
(andtry
, if the initializer could throw an error)Covers the type checking portion of rdar://70101851.