> once you have them, they're unsafe and unstructured i.e. you can forget to call them when you meant to.
At least in the case of asymmetric coroutines, such as what Lua supports, this is absolutely not the case. They're very structured--the caller invokes the callee, which always yields back to the caller. It's almost the same as simply calling another function. Yes, objects can persist beyond the invocation lifetime, but their lifetime is scoped to the lifetime of the coroutine object itself, which the caller manifestly holds. Async Rust is effectively built on asymmetric coroutines, AFAIU. The difficulties and differences lie in stack management--specifically, where and how that occurs, and how those details leak.
> Async/await does more than just concurrency; it explodes your function into a rather complicated state machine
All compilers explode functions into state machines. The question is how and where that state is managed. Non-async Rust, like many compiled languages, heavily relies on the underlying environment (hardware, operating system, ABI, etc) to set and restore stack and instruction pointers. A language like Lua uses virtual stacks, which are VM data structures that operate independently from the underlying "C stack". Async Rust sits somewhere in the middle.
Async Rust is fundamentally a practical compromise and has little to do with safety, per se. If anything, managing lifetime guarantees might have been easier if Rust was able to push stack management down further in the runtime model, separating concerns more fully. Doing so would have complicated FFI interoperability, however, so it was a non-starter. Much the same is true for Swift. But if you completely control the runtime environment, such as through a VM or gating FFI access through a bridge, then it's difficult to not choose coroutines.