I think it's not entirely fair to paint problems with rust's async features as an aversion to low-level-ness.
If anything, I think the issue with Rust's async is that it tries to be too high level.
In my experience with async rust, most of the difficulty comes from "spooky errors at a distance". I.e, you're writing some code which feels completely normal, and then suddenly you are hit with a large, obtuse error message about types and traits you didn't even know you were working with. The reason being, something went wrong with all the inference involved in implicitly wrapping everything in futures. And often it's not even related to the line of code you just wrote.
So in these cases, I think the issue is that rust tries very hard to wrap async in a very smooth, sugared syntax. It works fine in a lot of cases, but due to the precise nature of Rust, there are a cases where it doesn't work. And because the system relies on a lot of magic inference, the programmer has to hold a lot of assumptions and rules in their mind to work effectively with async.
I am not an expert, but from my experience it feels like async rust was a bit rushed. It should have been introduced as a more explicit syntax first, with sugar on top once the guard rails had better been figured out through experimentation.
That is under the assumption that future-based async in rust is a good idea at all. Rust's ownership model becomes intuitive very quickly in the single-threaded case, but at times it feels like a square peg in a round hole where async is concerned.
This may be a problem with async generally unless the language is designed specifically around async like Go is, which some profound tradeoffs to make it happen. (not criticizing that I think Go did a great job)
Zig managed to get async correct, if you ask me, but it's because I would call it "the lowest-level primitive to do what you need it to do" (shift the function frame over to a place that "might not be the stack"). If you think of it as a sugared "async" you will probably do it wrong.
After a lot of wrassling with it, once I realized that it was a very shallow abstraction over what the hardware is actually doing, everything clicked.
Having a global switch to switch the program over to async only works if all your concurrency primitives are aware of it: e.g. mutexes. Otherwise you get deadlocks. But Rust is designed to work with raw OS concurrency primitives. So that solution isn't available.
Zig's approach is really nice here, but it's also a notably easier problem to solve than rust has, since it's not trying to solve for parallel/concurrent safety.
Do you think Zig's approach would be feasible in Rust? I guess maybe you could do it, but the sync version would have to have the stricter constraints of the async version.
Not possible for the same reasons why just adding a borrow checker to C++ isn't possible. Mostly this comes down to incompatibility with existing code. Zig is just not memory safe, and the only solution would be to add a GC.
Assuming you mean seL4, that was formally verified by taking a manual specification of the L4 kernel and laboriously proving that the C code implements that specification. That is not at all what the compiler's lifetime/borrow check subsystems do for Rust. They know nothing about what the program is supposed to do; they only check that some relatively-simple rules are followed. That's why Rust requires so much less effort on the part of the programmer than formalisms that aim to prove programs correct.
Nobody is arguing anything on the level of doing a full on Sel4 -- but
Sel4 does have generalized no memory leak, no uaf guarantees as a subset of all specification proofs, so my point "memory safely is possible to bolt onto even C" is still valid.
I don't disagree. After working with Rust's model, which forces you to confront a lot of the tradeoffs and complexity, I'm more inclined to think async is a feature which should really be considered from the ground up when a language is being designed, to avoid painting oneself into a corner in the design space.
Yes, and (to be clear to people who don't click your link) it was removed for exactly the reason that the parent commenter mentioned. Rust's lack of a pervasive GC is not intended to suggest that GC is not generally useful (even in a systems context), only that it's too difficult to satisfy the sort of extreme control that people demand of a low-level language if they are forced to find ways to work around the pervasive GC. Which is to say, you need a way to turn off the GC, and if you can function with the GC off, then maybe you should push that as far as you can get away with, which is what Rust did.
Yeah I have done some work trying to optimize code written in GC'd runtimes, and eventually you will hit a wall.
GC is great for a lot of use-cases: for instance if I am writing a server-less application with mostly lambdas/cloud functions which pull data from a database and serve it to an HTTP response, the productivity gains you get with a nice high-level language will more than make up for a few pennies you might pay for a tracing garbage collector to run, and it's going to be plenty fast enough.
But when you have a case where every cycle/byte of memory counts, the GC is going to get in the way. Of course stop-the-world collectors are a catastrophe for performance, but even with modern generational GC it's essentially a part of your program's execution you didn't write and you have at best arms-reach control over which dictates hugely performance-impacting aspects of execution like memory layout.
Some GC'd languages have escape hatches to manipulate the GC process directly, but even those feel a bit like trying to drive a nail by hammering it with the butt of a screwdriver.
Yeah maybe not like go, but it's not impossible to imagine that you could create a language from the ground up with a rust-like ownership model and some async model - maybe not async await precisely.
If anything, I think the issue with Rust's async is that it tries to be too high level.
In my experience with async rust, most of the difficulty comes from "spooky errors at a distance". I.e, you're writing some code which feels completely normal, and then suddenly you are hit with a large, obtuse error message about types and traits you didn't even know you were working with. The reason being, something went wrong with all the inference involved in implicitly wrapping everything in futures. And often it's not even related to the line of code you just wrote.
So in these cases, I think the issue is that rust tries very hard to wrap async in a very smooth, sugared syntax. It works fine in a lot of cases, but due to the precise nature of Rust, there are a cases where it doesn't work. And because the system relies on a lot of magic inference, the programmer has to hold a lot of assumptions and rules in their mind to work effectively with async.
I am not an expert, but from my experience it feels like async rust was a bit rushed. It should have been introduced as a more explicit syntax first, with sugar on top once the guard rails had better been figured out through experimentation.
That is under the assumption that future-based async in rust is a good idea at all. Rust's ownership model becomes intuitive very quickly in the single-threaded case, but at times it feels like a square peg in a round hole where async is concerned.