Background
I have a task type that can both co_return
and co_yield
.
In LLVM the task works as expected and passes some early tests. In MSVC and GCC the code fails in the same manner (coincidence?).
Brief Problem
With the following test function:
Task<int> test_yielding()
{
co_yield 1;
co_return 2;
}
There are two values retrieved from a Task object.
auto a = co_await fn;
auto b = co_await fn;
The value of a is expected to be 1, the value of b is expected to be 2.
The result is tested against a + b == 3
.
The above test passes, however the following test fails:
auto res = co_await fn + co_await fn
The value of res for GCC and MSVC is 4. Both retrieved from the final co_return. As I understand it the first and second calls of co_await fn
should be 1 and 2 in either order.
In MSVC and GCC the code fails as they seems to reorder await_resume
, return_value
and yield_value
.
Details
I have run the code through clang tidy, PVS studio, enabled all the available sanitisers in LLVM, GCC, MSVC and nothing relevant pops up (just comments about destroy and resume not being noexcept).
I have several very similar tests: The relevant tests are:
Function:
Task<int> test_yielding()
{
co_yield 1;
co_return 2;
}
Test 1 (PASS):
Title("Test co_yield + co_return lvalue");
auto fn = test_yielding();
auto a = co_await fn;
auto b = co_await fn;
ASSERT(a + b == 3);
Test 2 (FAIL):
Title("Test co_yield + co_return rvalue");
auto fn = test_yielding();
auto res =
(
co_await fn +
co_await fn
);
ASSERT(res == 3);
The result of the test MSVC 1 (PASS):
---------------------------------
Title Test co_yield + co_return lvalue
---------------------------------
get_return_object: 02F01DA0
initial_suspend: 02F01DA0
await_transform: 02D03C80
AwaitAwaitable: await_ready: 02F01DA0
AwaitAwaitable: await_suspend: 02F01DA0
SetCurrent: 02F01DA0
ContinueWith: 02F01DA0
yield_value: 02F01DA0
SetValue: 02F01DA0
YieldAwaitable: await_ready: 02F01DA0
YieldAwaitable: await_suspend: 02F01DA0
ContinueWith: 02F01DA0
AwaitAwaitable: await_resume: 02F01DA0
GetValue: 02F01DA0
await_transform: 02D03C80
AwaitAwaitable: await_ready: 02F01DA0
AwaitAwaitable: await_suspend: 02F01DA0
SetCurrent: 02F01DA0
ContinueWith: 02F01DA0
YieldAwaitable: await_resume: 02F01DA0
return_value: 02F01DA0
SetValue: 02F01DA0
final_suspend: 02F01DA0
YieldAwaitable: await_ready: 02F01DA0
YieldAwaitable: await_suspend: 02F01DA0
ContinueWith: 02F01DA0
AwaitAwaitable: await_resume: 02F01DA0
GetValue: 02F01DA0
PASS test_task:323 a + b == 3
[ result = 3, expected = 3 ]
Destroy: 02F01DA0
The result of the test MSVC 2 (FAIL):
---------------------------------
Title Test co_yield + co_return rvalue
---------------------------------
get_return_object: 02F01CA0
initial_suspend: 02F01CA0
await_transform: 02D03C80
AwaitAwaitable: await_ready: 02F01CA0
AwaitAwaitable: await_suspend: 02F01CA0
SetCurrent: 02F01CA0
ContinueWith: 02F01CA0
yield_value: 02F01CA0
SetValue: 02F01CA0
YieldAwaitable: await_ready: 02F01CA0
YieldAwaitable: await_suspend: 02F01CA0
ContinueWith: 02F01CA0
await_transform: 02D03C80
AwaitAwaitable: await_ready: 02F01CA0
AwaitAwaitable: await_suspend: 02F01CA0
SetCurrent: 02F01CA0
ContinueWith: 02F01CA0
YieldAwaitable: await_resume: 02F01CA0
return_value: 02F01CA0
SetValue: 02F01CA0
final_suspend: 02F01CA0
YieldAwaitable: await_ready: 02F01CA0
YieldAwaitable: await_suspend: 02F01CA0
ContinueWith: 02F01CA0
AwaitAwaitable: await_resume: 02F01CA0
GetValue: 02F01CA0
AwaitAwaitable: await_resume: 02F01CA0
GetValue: 02F01CA0
FAIL test_task:342 res == 3
[ result = 4, expected = 3 ]
Destroy: 02F01CA0
If you look at the diff between working MSVC FAIL and MSVC PASS (with addresses corrected, the following appears): Which makes it clear that the following lines were reordered:
AwaitAwaitable: await_resume: 02901E20
GetValue: 02901E20
The source and results for LLVM and GCC are here.
Looking at the test 2 diff between the GCC FAIL and LLVM PASS: A very similar reording is occuring in GCC.
The highlighted lines in the diff are produced be the following source:
template <typename Promise>
struct AwaitAwaitable
{
Promise & m_promise;
bool await_ready() const noexcept
{
WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
return false;
}
void await_suspend(default_handle handle) noexcept
{
WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
m_promise.SetCurrent( m_promise.Handle() );
m_promise.ContinueWith( handle );
}
auto await_resume() const noexcept
{
WriteLine("AwaitAwaitable: ", __func__, ": ", &m_promise);
return m_promise.GetValue();
}
};
Does anybody know what is going on here, is this a compiler/library/user error?
The observed behaviour appears to be due to similar bugs in both GCC and MSVC in their handling of the addition-operator where the arguments are both
co_await
expressions.In this instance, both GCC and MSVC seem to be incorrectly sequencing the call to
await_resume()
for bothco_await
expressions after resumption from the second suspend-point (ie. just before the addition is performed).They should instead be sequencing the call to
await_resume()
for the firstco_await
expression (it is indeterminate which one) immediately after resuming from the first suspend-point and before beginning to evaluate the secondco_await
expression.