I think perhaps I am not explaining myself clearly enough.
It is exactly the same as the existing Rust borrow checker. For example if a mutable reference is used by an iterator, then it can’t also be used for something else. Each yield point is effectively ‘something else’, because other (unknown) code runs at that point until the coroutine resumes, and that other code might need the shared state, so that would conflict. All I am suggesting is extending Rust’s borrowing rules with some knowledge of coroutines and yield points. (By yield point, I mean either a yield (i.e. a switch to another stack), or a call to some function that might yield, even indirectly.)
Rust’s existing borrowing rules are effectively a lock which exists only logically in the compiler, and results in no lock/unlock code in the resulting executable. Similarly this proposal could be seen as coroutines asking for a lock on the shared state, and then releasing the lock before the next yield, but actually because no other coroutine can possibly run in that thread between one yield and the next, the compiler can prove that no-one else could contend on that lock so no lock/unlock code is emitted. Like the existing borrowing rules, where there is a risk of a lock contention, the compiler refuses to compile the code in question and gives an error message. So if I try to keep an iterator alive over a yield point, the compiler would reject that.
So there is never any contention on the ‘lock’, because the compiler doesn’t let a ‘lock’ be held over a yield point. There is no runtime overhead. No runtime checks are required.
Also, there is no runtime system required. The coroutines can manage themselves. They are effectively just a memory allocation, and can be managed like other memory allocations, although cleanup requires some knowledge of the stack data structure. (This is required if the coroutine is never resumed and the reference keeping it alive is dropped.) Yields always go to some specific other coroutine, often with a payload. There is no scheduler.
Coding in assembly this is all quite a natural thing to do. Creating a stack is trivial, as is switching to/from it. So when I say this is lightweight, I know that from the assembly level. It all gets conceptually weighed down by the abstractions built on top of it, but if we don’t attempt pre-emption, and we don’t attempt goroutines, it is all very very simple and natural.
However, details of syntax and how best to express this to the compiler, I’m not totally sure about.
Perhaps we need to be able to create a structure with a CoroutineShareble trait. References to a structure with this trait follow the special rules regarding borrows over yield-points. Otherwise passing data to a created coroutine is like passing to another thread (i.e. move or copy). But a reference to a CoroutineShareable structure could be passed unchanged to a coroutine and in that way mutation is possible from either the main stack or a coroutine stack, but with no borrows allowed over yield-points.
That is perhaps the most simple expression of it, but to make the grouping of operations on the shared data more explicit to avoid the spread-out atomic operation risk mentioned above, perhaps some additional syntax could be devised, e.g. some kind of start/end bracketting which won’t compile if there is a yield-point within the bracketted region.