Then literally everything is just sugar for assembly. Once a language is Turing Complete, it can do everything. A language addition is "significant" if it "significantly" changes how you do something. How much of a change is significant is highly subjective.
It's also very difficult to separate the "language" from the compiler, runtime, and the standard libraries. Especially if they're versioned and shipped together, they're intertwined in a way that makes separating them into separate concepts of questionable utility.
So I think the disconnect boils down to two things:
How important is marking unwinding control flow
Things like loop
, for
, or while
are prefix operations because they do control flow, but more specifically, because they do control flow which runs a block not exactly once.
.await
, ?
, and panicking, on the other hand, don't do any kind of control flow shenanigans that reorder the code written in your function. The only things which .await
or ?
do is either block forever, continue on with straightline control flow, or unwind the current context. This is exactly equivalent to any how any other potentially panic-unwinding function call works.
There are differences in how these unwinds are both implemented and covered -- .await
by dropping the Future
, ?
is reified into Result
, and panicking via some sidechannel which sets thread::is_panicking()
before dropping things off of your stack -- but from the viewpoint of this function, all three are the same API: instead of continuing straightline, your stack locals get dropped and you never resume.
async.await
isn't implemented this way, but you could implement it just by putting each async
block on its own thread and making .await
a blocking call, rather than the stackless generator transformation, and unwinding from that blocking call on synchronous cancellation.
Making only some unwinding visible
The point about "every" indexing comes from a position of "why mark more unwinding points if there still are implicit ones?"
If you allow for any code to unwind, then "all" of your code should be correct when unwound through, unless you have a reason to assert that it doesn't unwind. A lack of explicit marked unwind points isn't sufficient if there exists any way to unwind.
There is a difference between fully-implicit panic
unwinds and ?
or .await
propagated unwinding; you can turn off panic
unwinding, making it abort instead, and as such, panic
unwinding is (typically) only used for programming bugs, whereas .await
unwinding due to cancellation or ?
unwinding to return the break variant are considered normal, reasonable bits of control flow.
So, if a reason you picked up Rust pre-?
was that you could turn off unwinding and got to completely ignore that unwinding was a thing, you're not going to like ?
or .await
. Both of these contain at the core of their design that a little bit of unwinding, as a treat, is beneficial. There's a reason that exceptions are such a repeating part of language design -- unwinding is a very convenient way to write code, because it allows you to write the "happy path" and not clutter the code with saying if err != nil { return err }
everywhere.
If you model unwinding as just a function blocking forever, you don't have .await
or panicking doing any control flow. Actually unwinding is understandable as just an optimization over blocking forever; instead of blocking forever, everything on the stack is dropped, notifying the forever-blocked task that it won't be resuming, allowing the will-never-continue task to free any resources that it won't get to use anymore.
?
is much more difficult to justify modelling this way, because there's no clear "task" boundary to say is blocked while the dispatcher returns the error result, and unlike task cancellation, just blocking would likely cause normal executions to never complete, rather than just wasting resources. However, it is still possible to model this way, you just have to accept that in this case the you're-not-continuing notify is necessary to complete in order to resume the caller.
From this "unwinding is an optimization over infinite blocking" and the generally-considered-impossible task of proving real-world code completes and doesn't just spin or block forever, and you get Rust's current position of allowing the use of unwinding in very controlled ways.
You can write code, even Rust code, which is highly resilient to errors and doesn't rely on unwinding, but the general consensus of not just the Rust community but the programming community at large is that for general-purpose development, allowing some amount of unwinding makes code much simpler to develop.
Perhaps in another 20 years this will shift, and unwinding will be viewed similarly to how null
able-everything is today, but we're not there yet, and today, structured and targeted use of unwinding generally improves code.