This is an archive of the discontinued LLVM Phabricator instance.

[C++20] [Coroutines] Mark coroutine done if unhandled_exception throws
ClosedPublic

Authored by ChuanqiXu on Dec 6 2021, 10:15 PM.

Details

Summary

[[dcl.fct.def.coroutine]/p14](https://eel.is/c++draft/dcl.fct.def.coroutine#14) says:

If the evaluation of the expression promise.unhandled_­exception() exits via an exception, the coroutine is considered suspended at the final suspend point.

However, this is not implemented in clang. We could observe this from: https://godbolt.org/z/Edr59d5Y6.

This patch would implement this feature by marking the coroutine as done at the place of coro.end(frame, /*InUnwindPath=*/true ).

After this patch, the behavior of this example would be the same with GCC: https://godbolt.org/z/rh86xKf85.

Test Plan: check-all, https://godbolt.org/z/rh86xKf85 and an internal coroutine library

Diff Detail

Event Timeline

ChuanqiXu created this revision.Dec 6 2021, 10:15 PM
ChuanqiXu requested review of this revision.Dec 6 2021, 10:15 PM

I agree that you shouldn't call suspend, but doesn't coro.end have the behavior of marking the coroutine done? Should we just be calling coro.end on this path?

llvm/docs/Coroutines.rst
1667

"The '`llvm.coro.mark.done' intrinsic marks that the current coroutine is complete. 'llvm.coro.mark.done`' is lowered during coroutine splitting and must not appear outside of an unsplit coroutine. This intrinsic is only supported for switched-resume coroutines."

1674

"'`llvm.coro.mark.done`' doesn't require any arguments. There is no ambiguity about which coroutine is meant because unsplit coroutines in LLVM cannot be broken up or inlined."

ChuanqiXu updated this revision to Diff 392302.Dec 7 2021, 12:58 AM
ChuanqiXu edited the summary of this revision. (Show Details)

Use llvm.coro.end(frame, /*InUnwindPath=*/true) instead of new coroutine intrinsics.

I agree that you shouldn't call suspend, but doesn't coro.end have the behavior of marking the coroutine done? Should we just be calling coro.end on this path?

@rjmccall great insight! coro.end wouldn't marking the coroutine done previously. But its place is perfect to do so. I have added the codes to mark the coroutine as done for coro.end in the unwind path. And I have checked the behavior. The exception happened earlier wouldn't run into the path of coro.end exists. So the behavior is satisfying. The only defect might be that the behavior is C++'s semantic. Although, there might be a problem if there is another language also uses switch-resumed ABI one day. I think the current approach is much better at least it has and would generate less code.

I agree that you shouldn't call suspend, but doesn't coro.end have the behavior of marking the coroutine done? Should we just be calling coro.end on this path?

@rjmccall great insight! coro.end wouldn't marking the coroutine done previously. But its place is perfect to do so. I have added the codes to mark the coroutine as done for coro.end in the unwind path. And I have checked the behavior. The exception happened earlier wouldn't run into the path of coro.end exists. So the behavior is satisfying. The only defect might be that the behavior is C++'s semantic. Although, there might be a problem if there is another language also uses switch-resumed ABI one day. I think the current approach is much better at least it has and would generate less code.

Okay. Well, I'm glad it works. I guess I find it a little strange that coro.end doesn't already mark the coroutine done. I guess the normal C++ lowering always generates a final suspension before reaching a non-unwind coro.end? There are a lot of semantic differences between throwing out of a C++ coroutine and returning out of it that I always find surprising.

Something ultimately invalidates the coroutine handle by deallocating the frame, right? When does that happen when throwing out of a coroutine?

Okay. Well, I'm glad it works. I guess I find it a little strange that coro.end doesn't already mark the coroutine done. I guess the normal C++ lowering always generates a final suspension before reaching a non-unwind coro.end?

Yes, there is always a final suspend before coro.end in normal path. So that the coroutine would be marked done all the time except unhandled_exception throws.

There are a lot of semantic differences between throwing out of a C++ coroutine and returning out of it that I always find surprising.

Yes, I guess this is the reason why the coroutine should be marked done if unhandled_exception throws.

Something ultimately invalidates the coroutine handle by deallocating the frame, right?

Yes, the coroutine frame is invalided by deallocating. Users (coroutine library writers) need to deallocate it by hand usually (by call coroutine_handle<>::destroy()). The coroutine would get destroyed automatically if the coroutine completes and not suspended at the final suspend point.

When does that happen when throwing out of a coroutine?

After throwing out of a coroutine, the coroutine frame is still valid. And the owner is responsible to destroy it.

Okay. Well, I'm glad it works. I guess I find it a little strange that coro.end doesn't already mark the coroutine done. I guess the normal C++ lowering always generates a final suspension before reaching a non-unwind coro.end?

Yes, there is always a final suspend before coro.end in normal path. So that the coroutine would be marked done all the time except unhandled_exception throws.

Ah, I see. This is the one place where an exception can be thrown before we reach that point.

You should update the coroutines documentation to mention this effect of llvm.coro.end(true).

llvm/lib/Transforms/Coroutines/CoroSplit.cpp
313
314
325

We need to do this store elsewhere, right, like during final suspends? Can we make a common function for it?

ChuanqiXu updated this revision to Diff 392669.Dec 8 2021, 12:35 AM

Address comments

ChuanqiXu marked 2 inline comments as done.Dec 8 2021, 12:39 AM
ChuanqiXu added inline comments.
llvm/docs/Coroutines.rst
1396

Maybe I need to explain why it would work in start (ramp) function. Generally, the ramp function is very small. But in case the initial_suspend of the coroutine wouldn't always suspend, the start function could be as large as the original function. In this case, it should remain the original semantics.

llvm/lib/Transforms/Coroutines/CoroSplit.cpp
325

I am not sure if I understand your point. I think the place where the coro.end lives should be the right place. The common function is made.

rjmccall accepted this revision.Dec 8 2021, 11:45 AM

Minor suggestion in the docs, but otherwise LGTM.

llvm/docs/Coroutines.rst
1382

How about:

In the unwind path (when the argument is `true`), `coro.end` will mark the coroutine as done, making it undefined behavior to resume the coroutine again and causing `llvm.coro.done` to return `true`.  This is not necessary in the normal path because the coroutine will already be marked as done by the final suspend.
1396

That makes sense. Honestly, it feels very strange to me that there are any differences between the ramp function and the resume/destroy functions in the handling of coro.end.

llvm/lib/Transforms/Coroutines/CoroSplit.cpp
325

Yes, thank you, this is all I wanted.

This revision is now accepted and ready to land.Dec 8 2021, 11:45 AM
ChuanqiXu updated this revision to Diff 393039.Dec 8 2021, 10:57 PM

Address comments.

Thanks for reviewing this!

This revision was landed with ongoing or failed builds.Dec 8 2021, 11:07 PM
This revision was automatically updated to reflect the committed changes.