Skip to content

[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

Merged
merged 9 commits into from
Nov 5, 2020

Conversation

DougGregor
Copy link
Member

@DougGregor DougGregor commented Nov 4, 2020

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:

  • Declaring async let declarations within asynchronous functions
  • Ensuring that access to an async let declaration is covered by await (and try, if the initializer could throw an error)
  • Treating the initializer as a concurrently-executing child task, by injecting it into an escaping autoclosure, which (e.g.) takes it out of the actor isolation domain

Covers the type checking portion of rdar://70101851.

@DougGregor DougGregor marked this pull request as draft November 4, 2020 16:07
@rintaro
Copy link
Member

rintaro commented Nov 5, 2020

Could you add test cases for multiple patterns? like

async let veggies = try chopVegetables(), meat = marinateMeat()

Edit: I see you have async let (_, _) = (1, 2), y2 = 7, but this is a (different?) error case.

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'.
@DougGregor DougGregor changed the title [Concurrency] Parse 'async let' declarations. [Concurrency] Implement type checking for 'async let' declarations. Nov 5, 2020
@DougGregor DougGregor marked this pull request as ready for review November 5, 2020 08:07
@DougGregor
Copy link
Member Author

Could you add test cases for multiple patterns? like

async let veggies = try chopVegetables(), meat = marinateMeat()

Sure, I've added it, plus a bunch of other tests.

@DougGregor
Copy link
Member Author

@swift-ci please smoke 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.

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}}
Copy link
Contributor

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 🤔

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 agree that we could do better with the "why?" here, e.g., we could call out that we're in a child task.

@DougGregor DougGregor merged commit 203d2c2 into swiftlang:main Nov 5, 2020
@DougGregor DougGregor deleted the async-let branch November 5, 2020 16:44

// REQUIRES: concurrency

async let x = 1 // okay
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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.

Comment on lines +159 to +161
async let x = await getInt()
print(x) // expected-error{{reference to async let 'x' is not marked with 'await'}}
print(await x)
Copy link
Contributor

@kastiglione kastiglione Nov 5, 2020

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)

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 (intentionally) deviated from the proposal document, because as I was writing the tests I felt like skipping the await was arbitrary and confusing.

Copy link
Contributor

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 awaits 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.

Copy link
Member Author

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.

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