Skip to content

Changes to coroutine promise object lost in return_value if not used, when -O1 or higher is enabled (since Clang 18) #186311

@hach-que

Description

@hach-que

Consider the following code:

#include <string>
#include <coroutine>
#include <cstdio>
#include <memory>
#include <optional>

#define debug_promise_state(context, promise) \
    printf("(%p) %s: promise memory_zero_test=%d has_immediate_result=%s\n", promise, context, promise->memory_zero_test, promise->has_immediate_result ? "yes" : "no")

template<typename T>
struct promise_base
{
    int memory_zero_test;
    bool has_immediate_result;
    std::optional<T> immediate_result;

    promise_base()
        : memory_zero_test(0xdeadbeef)
        , has_immediate_result()
        , immediate_result()
    {
    }
};

template<typename T>
struct task
{
private:
    promise_base<T>* source;
    bool has_promise_reference;

public:
    task()
        : source()
        , has_promise_reference(false)
    {
    }

    task(promise_base<T>* source)
        : source(source)
        , has_promise_reference(true)
    {
    }

    task(const task&) = delete;
    task(task&&) = delete;
    task& operator=(const task&) = delete;
    task& operator=(task&&) = delete;

    bool await_ready()
    {
        debug_promise_state("await_ready", this->source);

        if (this->has_promise_reference && 
            this->source->has_immediate_result) {
            printf("task: await_ready with value reference\n");
            return true;
        } else {
            printf("task: await_ready with heap reference\n");
            return false;
        }
    }

    void await_suspend(auto handle)
    {
        debug_promise_state("await_suspend", this->source);

        handle.resume();
    }

    T await_resume()
    {
        debug_promise_state("await_resume", this->source);

        if (this->has_promise_reference && 
            this->source->has_immediate_result) {
            printf("task: await_resume with value_ref\n");
            return this->source->immediate_result.value();
        } else {
            printf("task: await_resume with heap_ref\n");
            return 10;
        }
    }
};

template<typename T>
struct promise;

namespace std {

template<typename T>
struct coroutine_traits<task<T>>
{
    using promise_type = ::promise<T>;
};

}

template<typename T>
struct promise : public promise_base<T>
{
    promise() = default;
    promise(const promise&) = delete;
    promise(promise&&) = delete;
    ~promise() = default;

    promise& operator=(const promise&) = delete;
    promise& operator=(promise&&) = delete;

    task<T> get_return_object()
    {
        debug_promise_state("get_return_object", this);
        return task<T>(this);
    }
    std::suspend_never initial_suspend() noexcept { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void return_value(T&& value)
    {
        printf("setting return_value with promise %p\n", this);

        this->has_immediate_result = true;
        this->immediate_result = value;
        
        // uncomment this line to get the correct value path behaviour.
        //printf("using immediate_result field %s\n", this->immediate_result ? "yes" : "no");
    }
    void unhandled_exception() {}
};

task<int> test()
{
    co_return 5;
}

task<int> test2()
{
    int blah = co_await test();
    co_return blah;
}

int main()
{
    auto task = test2();
    return 0;
}

The correct behaviour is to emit "task: await_ready with value reference" because has_immediate_result is set to true, and then checked with await_ready. For some reason the optimizer is removing the assignment of has_immediate_result if it's not immediately used in return_value.

This happens on trunk (on Godbolt) and all versions of Clang since Clang 18 (where it regressed). Clang 17 has the correct behaviour with the -O1.

Note the commented out printf call inside return_value. This code produces different results with -O0, and with the printf uncommented out. MSVC gets this correct with optimizations turned on (/O2).

Clang trunk -std=c++20 -O0 (correct)

(0x58b8bb19a2c0) get_return_object: promise memory_zero_test=-559038737 has_immediate_result=no
(0x58b8bb19b310) get_return_object: promise memory_zero_test=-559038737 has_immediate_result=no
setting return_value with promise 0x58b8bb19b310
(0x58b8bb19b310) await_ready: promise memory_zero_test=-559038737 has_immediate_result=yes
task: await_ready with value reference
(0x58b8bb19b310) await_resume: promise memory_zero_test=-559038737 has_immediate_result=yes
task: await_resume with value_ref
setting return_value with promise 0x58b8bb19a2c0

Clang trunk -std=c++20 -O1 (incorrect❗)

(0x5d08d38cc2c0) get_return_object: promise memory_zero_test=-559038737 has_immediate_result=no
(0x7fff1dfb23b0) get_return_object: promise memory_zero_test=-559038737 has_immediate_result=no
setting return_value with promise 0x7fff1dfb23b0
(0x7fff1dfb23b0) await_ready: promise memory_zero_test=-559038737 has_immediate_result=no
task: await_ready with heap reference
(0x7fff1dfb23b0) await_suspend: promise memory_zero_test=-559038737 has_immediate_result=no
(0x7fff1dfb23b0) await_resume: promise memory_zero_test=-559038737 has_immediate_result=no
task: await_resume with heap_ref
setting return_value with promise 0x5d08d38cc2c0

Clang trunk -std=c++20 -O1 with printf call uncommented (correct)

(0x578c364ec2c0) get_return_object: promise memory_zero_test=-559038737 has_immediate_result=no
(0x7fff28dec930) get_return_object: promise memory_zero_test=-559038737 has_immediate_result=no
setting return_value with promise 0x7fff28dec930
using immediate_result field yes
(0x7fff28dec930) await_ready: promise memory_zero_test=-559038737 has_immediate_result=yes
task: await_ready with value reference
(0x7fff28dec930) await_resume: promise memory_zero_test=-559038737 has_immediate_result=yes
task: await_resume with value_ref
setting return_value with promise 0x578c364ec2c0
using immediate_result field yes

MSVC with /O2 (optimizations enabled, correct)

(000001DD0B7CF690) get_return_object: promise memory_zero_test=-559038737 has_immediate_result=no
(000001DD0B7CAD30) get_return_object: promise memory_zero_test=-559038737 has_immediate_result=no
setting return_value with promise 000001DD0B7CAD30
(000001DD0B7CAD30) await_ready: promise memory_zero_test=-559038737 has_immediate_result=yes
task: await_ready with value reference
(000001DD0B7CAD30) await_resume: promise memory_zero_test=-559038737 has_immediate_result=yes
task: await_resume with value_ref
setting return_value with promise 000001DD0B7CF690

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions