Skip to content

Tracking issue for RFC 2033: Experimentally add coroutines to Rust #43122

Open

Description

@aturon

RFC.

This is an experimental RFC, which means that we have enough confidence in the overall direction that we're willing to land an early implementation to gain experience. However, a complete RFC will be required before any stabilization.

This issue tracks the initial implementation.

related issues

See also:

Activity

  1. added
    B-RFC-approvedBlocker: Approved by a merged RFC but not yet implemented.
    T-langRelevant to the language team
    on Jul 8, 2017
  2. alexcrichton commented on Jul 9, 2017

    @alexcrichton
    Member

    cc #43076, an initial implementation

  3. mitranim commented on Jul 25, 2017

    @mitranim
    Contributor

    Copied from #43076:


    I'm using this branch for stream-heavy data processing. By streams I mean iterators with blocking FS calls. Because Generator is missing an Iterator or IntoIterator implementation, you must call your own wrapper. Zoxc kindly provided an example, but it's quite unergonomic. Consider:

    Python:

    def my_iter(iter):
        for value in iter:
            yield value

    Rust with generators:

    fn my_iter<A, I: Iterator<Item=A>>(iter: I) -> impl Iterator<Item=A> {
        gen_to_iter(move || {
            for value in iter {
                yield value;
            }
        })
    }

    Two extra steps: inner closure + wrapper, and, worse, you have to write the wrapper yourself. We should be able to do better.

    TL:DR: There should be a built-in solution for Generator→Iterator.

  4. added
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    on Jul 27, 2017
  5. silene commented on Aug 21, 2017

    @silene

    I was a bit surprised that, during the RFC discussion, links to the C++ world seemed to reference documents dating back from 2015. There have been some progress since then. The latest draft TS for coroutines in C++ is n4680. I guess the content of that draft TS will be discussed again when the complete RFC for Rust's coroutines is worded, so here are some of the salient points.

    First, it envisions coroutines in a way similar to what this experimental RFC proposes, that is, they are stackless state machines. A function is a coroutine if and only if its body contains the co_await keyword somewhere (or co_yield which is just syntactic sugar for co_await, or co_return). Any occurrence of co_await in the body marks a suspension point where control is returned to the caller.

    The object passed to co_await should provide three methods. The first one tells the state machine whether the suspension should be skipped and the coroutine immediately resumed (kind of a degenerate case). The second method is executed before returning control to the caller; it is meant to be used for chaining asynchronous tasks, handling recursive calls, etc. The third method is executed once the coroutine is resumed, e.g. to construct the value returned by the co_await expression. When implementing most generators, these three methods would have trivial bodies, respectively { return false; }, {}, and {}.

    Various customization mechanisms are also provided. They tell how to construct the object received by the caller, how to allocate the local variables of the state machine, what to do at the start of the coroutine (e.g. immediately suspend), what to do at the end, what do to in case of an unhandled exception, what to do with the value passed to co_yield or co_return (how yielded values are passed back to the caller is completely controlled by the code).

  6. arielb1 commented on Sep 20, 2017

    @arielb1
    Contributor

    One subtle point that came up is how we handle the partially-empty boxes created inside of box statements with respect to OIBITs/borrows.

    For example, if we have something like:

    fn foo(...) -> Foo<...> {}
    fn bar(...) -> Bar<...> {}
    box (foo(...), yield, bar(...))

    Then at the yield point, the generator obviously contains a live Foo<...> for OIBIT and borrow purposes. It also contains a semi-empty Box<(Foo<...>, (), Bar<...>)>, and we have to decide whether we should have that mean that it is to be treated like it contains a Box, just the Foo<...>, or something else.

  7. masonk commented on Jan 7, 2018

    @masonk

    I might be missing something in the RFC, but based on the definition of resume in the Generator struct, and the given examples, it looks like these generators don't have two way communication. Ideally this language construct would allow us to yield values out and resume values into the generator.

    Here's an example of implementing the async/await pattern using coroutines in ES6. The generator yields Promises and the coroutine resumes the generator with the unwrapped value of a Promise each time the Promise completes. There is no way this pattern could have been implemented without the two-way communication.

    Rust has a problem here because what's the type of resume? In the ES6 example, the generator always yields out some kind of Promise and is always resumed with the unwrapped value of the Promise. However the contained type changes on each line. In other words, first it yields a Promise<X> and is resumed with an X, and then it yields a Promise<Y> and is resumed with a Y. I can imagine various ways of declaring that this generator first yields a Wrapper<X> and then a Wrapper<Y>, and expects to be resumed with an X and then a Y, but I can't imagine how the compiler will prove that this is what happens when the code runs.

    TL;DR:
    yield value is the less interesting half. It has the potential to be a much more ergonomic way to build an Iterator, but nothing more.

    let resumedValue = yield value; is the fun half. It's what turns on the unique flow control possibilities of coroutines.

    (Here are some more very interesting ideas for how to use two-way coroutines.)

  8. mikeyhew commented on Jan 19, 2018

    @mikeyhew
    Contributor

    @arielb1

    Then at the yield point, the generator obviously contains a live Foo<...> for OIBIT and borrow purposes. It also contains a semi-empty Box<(Foo<...>, (), Bar<...>)>, and we have to decide whether we should have that mean that it is to be treated like it contains a Box, just the Foo<...>, or something else.

    I don't know what you mean by "OIBIT". But at the yield point, you do not have a Box<(Foo<...>, (), Bar<...>)> yet. You have a <Box<(Foo<...>, (), Bar<...>)> as Boxed>::Place and a Foo<...> that would need to be dropped if the generator were dropped before resuming.

  9. clarfonthey commented on Feb 11, 2018

    @clarfonthey
    Contributor

    Looking at the API, it doesn't seem very ergonomic/idiomatic that you have to check if resume returns Yielded or Complete every single iteration. What makes the most sense is two methods:

    fn resume(&mut self) -> Option<Self::Yield>;
    fn await_done(self) -> Self::Return;
    

    Note that this would technically require adding an additional state to closure-based generators which holds the return value, instead of immediately returning it. This would make futures and iterators more ergonomic, though.

    I also think that explicitly clarifying that dropping a Generator does not exhaust it, stopping it entirely. This makes sense if we view the generator as a channel: resume requests a value from the channel, await_done waits until the channel is closed and returns a final state, and drop simply closes the channel.

  10. janhohenheim commented on Feb 28, 2018

    @janhohenheim
    Contributor

    Has there been any progress regarding the generator -> iterator conversion? If not, is there any active discussion about it somewhere? It would be useful to link it.
    @Nemikolh and @uHOOCCOOHu, I'm curious about why you disagree with @clarcharr's suggestion. Care to share your thoughts?

  11. phaux commented on Mar 2, 2018

    @phaux

    Has there been any progress regarding the generator -> iterator conversion? If not, is there any active discussion about it somewhere?

    https://internals.rust-lang.org/t/pre-rfc-generator-integration-with-for-loops/6625

  12. Flupp commented on Mar 28, 2018

    @Flupp

    I was looking at the current Generator-API and immediately felt uneasy when I read

    If Complete is returned then the generator has completely finished with the value provided. It is invalid for the generator to be resumed again.

    Instead of relying on the programmer to not resume after completion, I would strongly prefer if this was ensured by the compiler. This is easily possible by using slightly different types:

    pub enum GeneratorState<S, Y, R> {
        Yielded(S, Y),
        Complete(R),
    }
    
    pub trait Generator where Self: std::marker::Sized {
        type Yield;
        type Return;
        fn resume(self) -> GeneratorState<Self, Self::Yield, Self::Return>;
    }

    (see this rust playground for a small usage example)

    The current API documentation also states:

    This function may panic if it is called after the Complete variant has been returned previously. While generator literals in the language are guaranteed to panic on resuming after Complete, this is not guaranteed for all implementations of the Generator trait.

    So you might not immediately notice a resume-after-completion at runtime even when it actually occurs. A panic on resume-after-completion needs additional checks to be performed by resume, which would not be necessary with the above idea.

    In fact, the same idea was already brought up in a different context, however, the focus of this discussion was not on type safety.

    I assume there are good reasons for the current API. Nevertheless I think it is worth (re)considering the above idea to prevent resume-after-completion. This protects the programmer from a class of mistakes similar to use-after-free, which is already successfully prevented by rust.

  13. Nemo157 commented on Mar 28, 2018

    @Nemo157
    Contributor

    I too would have preferred a similar construction for the compile time safety. Unfortunately, that construction doesn't work with immovable generators, once they have been resumed they can't ever be passed by value. I can't think of a way to encode that constraint in a similar way for pinned references, it seems you need some kind of affine reference that you can pass in and recieve back in the GeneratorState::Yielded variant rather than the current lifetime scoped Pin reference.

  14. 185 remaining items

  15. added a commit that references this issue on Mar 20, 2025
  16. added 2 commits that reference this issue on Mar 20, 2025
  17. added a commit that references this issue on Mar 21, 2025
  18. added 2 commits that reference this issue on Apr 3, 2025
  19. discreaminant2809 commented on Apr 15, 2025

    @discreaminant2809

    There are multiple tracking issues of it so idk where it should appropriately be.

    The compiler not being able to infer type in this case is quite wild.

    #![feature(coroutines)]
    #![feature(coroutine_trait)]
    #![feature(stmt_expr_attributes)]
    
    use std::ops::Coroutine;
    
    fn main() {
        fn assert_coroutine(_: impl Coroutine<i32, Yield = ()>) {}
        assert_coroutine(#[coroutine] |x| {
            x.count_ones();
        });
    }
    error[E0282]: type annotations needed
      --> src/lib.rs:9:36
       |
    9  |     assert_coroutine(#[coroutine] |x| {
       |                                    ^
    10 |         x.count_ones();
       |         - type must be known at this point
       |
    help: consider giving this closure parameter an explicit type
       |
    9  |     assert_coroutine(#[coroutine] |x: /* Type */| {
       |                                     ++++++++++++
    
    For more information about this error, try `rustc --explain E0282`.
    error: could not compile `playground` (lib) due to 1 previous error
    
  20. eholk commented on Apr 15, 2025

    @eholk
    Contributor

    @discreaminant2809 - What do you think the type of x should be in that context?

  21. eholk commented on Apr 15, 2025

    @eholk
    Contributor

    I have been programming in Rust for 5 years. I am making an experimental deterministic runtime with Rust. Coroutines/generators are instrumental to my engine. How would one get involved in the creation of the language or attend a design meeting?

    Best regards, Sol Midnight

    @solmidnight - I'd recommend reaching out in the t-lang Zulip channel. There are some other places to go from there, but that's a good starting point.

  22. discreaminant2809 commented on Apr 15, 2025

    @discreaminant2809

    @discreaminant2809 - What do you think the type of x should be in that context?

    The signature: impl Coroutine<i32, Yield = ()>
    x is at the resume argument position, which is i32 in this case (the resume argument type is the only generic so the one without = is it).

  23. DanielFrantes commented on Jun 29, 2025

    @DanielFrantes

    @discreaminant2809 Hello anything that needs to be tested?

  24. cosmicexplorer commented on Dec 4, 2025

    @cosmicexplorer

    I'm glad I actually read through the comments, because I now understand the contention over the interface. I would like to further complicate the discussion by proposing yet another distinct trait, but first I want to propose something entirely different.

    Construction of Coroutines from unsafe Primitives

    My understanding is that coroutines require specific compiler support, which is why they can't be polyfilled in user code. From the eRFC, that compiler support appears to specifically consist of the construction of a state machine which closes over variables and function logic.

    I see much debate over the appropriate high-level trait to wrap this in, and the various use cases described in this thread seem to present directly conflicting constraints. But to my understanding, the wrapper trait does not require specific compiler support. So I'm curious if we could avoid blocking on that decision by exposing a(n unsafe) lower-level interface to the compiler-specific part.

    Analogy to RawWaker{,VTable}

    In particular, I'm struck by the way async and the core::future::Future trait are progressively built up from lower-level primitives, and in particular by the RawWaker object, which directly exposes a type-erased vtable of unsafe function calls (see RawWakerVTable::new().

    I'm surprised that the coroutine implementation has only the single high-level ops::Coroutine trait, without any such lower-level unsafe interface that encapsulates the state machine and closures that only the compiler can build. The MIR phase's CoroutineArgs type appears to describe exactly that state machine:

    #[extension(pub trait CoroutineArgsExt<'tcx>)]
    impl<'tcx> ty::CoroutineArgs<TyCtxt<'tcx>> {
    /// Coroutine has not been resumed yet.
    const UNRESUMED: usize = 0;
    /// Coroutine has returned or is completed.
    const RETURNED: usize = 1;
    /// Coroutine has been poisoned.
    const POISONED: usize = 2;
    /// Number of variants to reserve in coroutine state. Corresponds to
    /// `UNRESUMED` (beginning of a coroutine) and `RETURNED`/`POISONED`
    /// (end of a coroutine) states.
    const RESERVED_VARIANTS: usize = 3;
    const UNRESUMED_NAME: &'static str = "Unresumed";
    const RETURNED_NAME: &'static str = "Returned";
    const POISONED_NAME: &'static str = "Panicked";

    Proposal: Explicit State Machine with unsafe Function Pointers

    I understand that this rustc internal MIR struct cannot just be translated to user code, especially because it maps state machine variants to types, which is a form of dependent typing that Rust cannot currently express in user code. Is it at all possible to adapt the approach of RawWakerVTable, which only works with type-erased function pointers?

    It strikes me that the coroutine state machine is a kind of inverse of the RawWakerVTable, in that the coroutine state machine provides unsafe untyped function pointers for the user to invoke. Since we can't yet agree on a single type signature for inputs and outputs, could we instead just return an untyped argument, and let the user do an unsafe cast to the correct type?

    The 2020 RFC implemented by @jonas-schievink as per #43122 (comment) describes the MIR transformation in a way that is very close to this: https://github.com/semtexzv/rfcs/blob/master/text/0000-unified_coroutines.md#addendum-samples Instead of a generic type-safe trait, could we produce a concrete struct that operates upon pointers?

    I understand that there remains an intricacy about how to handle sized arguments and return values in this model, which I don't yet know how to resolve. RawWakerVTable can force its vtable to operate upon pointers--perhaps the coroutine state machine can require the user to provide Pinned pointers for resume, yield, and return values?

  25. cosmicexplorer commented on Dec 4, 2025

    @cosmicexplorer

    Separately, I want to note yet another use case for coroutines that motivates yet another way to arrange type bounds in the Coroutine trait (sorry!).

    Use Case: Buffered Streaming

    This extends @zesterer's proposed interface at #43122 (comment):

    pub trait Generator {
        type Yield;
        type Return;
        type Resume<'a>;
    
        fn resume<'a>(self: Pin<&mut Self>, arg: Self::Resume<'a>) -> GeneratorState<Self::Yield, Self::Return>;
    }

    On the generator side, the value you get out of an invocation of yield(_) would be a value that lives until the next yield. For example:

    let resumed_value = yield(1);
    // <-- Legal to use `resumed_value` here
    let resumed_value2 = yield(2);
    // <-- Illegal to use `resumed_value` here, the second `yield` invalidates the first resume value

    This seems of questionable value, except for the fact that this design allows you to repeatedly pass some sort of long-lived state/context into a generator for it to operate upon.

    I have encountered this pattern repeatedly in the context of buffered streaming, where the user provides an allocated memory region, and the stream process populates it with a series of outputs, stopping when it runs out of space. While the Read trait largely achieves this for byte streams, it fails to represent streams of delimited messages. My current example is getdents() (see rust-lang/libc#4522), which accepts a buffer from the user, and writes a series of directory entries.

    I am currently adapting this chunked streaming process for directory entries from getdents() to conform to the Iterator interface, so that it can be used to implement fs::read_dir() in the stdlib. One issue that arises from ReadDir and DirEntry having no lifetime parameters is that they can live past the Iterator::next() invocation that produced them. As per the linked comment:

    Being able to communicate with a generator using some extra context where said context has a lifetime bound to the caller of resume makes generators immensely more useful for many applications, particularly those with deferred execution patterns that need to do some useful work on the context between invocations of resume. The only ways to do this today require a lot of unsafe (which can't be hidden from the user) and have extremely questionable soundness.

    Because the buffer provided to getdents() has its own lifetime distinct from the directory stream as well as individual DirEntrys, it needs to deploy a workaround, in case the user doesn't drop all the DirEntrys associated with a getdents() chunk before the ReadDir iterator needs to call getdents() again. In this specific case, the getdents() buffer can be cloned, allowing the remaining DirEntrys to continue using the previous chunk without blocking forward progress. But in general, representing the lifetime constraints of the streaming process itself as distinct from the buffer it uses to store its output avoids these kinds of workarounds and enables much more generic library APIs.

    Case Studies: Pipelined Processing

    One particular use case that demonstrates the value of giving the stream buffer its own lifetime is pipelined processing of stream chunks. In the case of getdents(), chunks of directory entries can be processed across a thread pool, instead of interspersing directory traversal with e.g. regex filtering logic in the same OS thread. This is particularly important for recursive directory traversal such as in the ignore crate, where lower levels of the directory hierarchy cannot be traversed until their parent directory is scanned. Parallel processing of getdents() chunks weakens the serial data dependency inherent in tree traversals.

    For another example, I am currently reworking my parallel zip file extraction technique for the third time to compare performance using in-memory queues over OS pipes (zip-rs/zip2#236). Each zip file entry is copied into a worker thread's buffer with pread(), and the decompressed contents are then written out to yet another buffer from another worker thread, and finally written to the output file from yet another worker thread. OS pipes currently perform most of the buffering, and also provide backpressure, but that buffering is kind of mysterious and induces control flow that's not directly visible in my code. For this third and hopefully final rewrite, I'm planning to introduce a coroutine-like trait that looks very much like the proposal below, so that I can express buffered pipelines with lifetimes, and produce backpressure that I can control, measure, and optimize in my own code instead of leaving it up to the OS.

    Proposal: More Lifetimes Means More Betterer

    The proposed extension is very small, but specifically allows for buffered streaming use cases in which the yielded value contains a reference to the provided buffer argument:

    pub trait Coroutine {
      type Yield<'a>;
      type Return<'s>;
      type Resume<'a>;
    
      fn resume<'a, 's>(self: Pin<&'s mut Self>, arg: Self::Resume<'a>) -> CoroutineState<Self::Yield<'a>, Self::Return<'s>>;
    }

    Note too the lifetime relation from self to Return, which enables moving data out of the coroutine when it completes. The lifetime parameters seem excessive, but they can be easily ignored when not needed:

    impl Coroutine for MyCoro {
      type Yield<'a> = usize;
      type Return<'s> = ();
      type Resume<'a> = u8;
    
      fn resume<'a, 's>(self: Pin<&'s mut Self>, arg: Self::Resume<'a>) -> CoroutineState<Self::Yield<'a>, Self::Return<'s>> {
        todo!()
      }
    }
  26. tgross35 commented on Jan 3, 2026

    @tgross35
    Member

    @cosmicexplorer those are some reasonable considerations; it may be worth bringing them up on Zulip (#t-lang) since much more design work happens there than in GitHub issues.

  27. cosmicexplorer commented on Jan 5, 2026

    @cosmicexplorer

    Thanks so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-coroutinesArea: CoroutinesB-experimentalBlocker: In-tree experiment; RFC pending, not yet approved or unneeded (requires FCP to stabilize).B-unstableBlocker: Implemented in the nightly compiler and unstable.C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCF-coroutines`#![feature(coroutines)]`S-tracking-design-concernsStatus: There are blocking design concerns.T-langRelevant to the language team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions