Although not part of the goal, it also mentions `!Destruct`/"must-move types", aka linear types: Instead of there always being a way to drop values without providing any arguments, if you wanna get rid of a value of a linear type you have to call a function that takes it by value.
For context, the reason this would be really nice is that it would enable API designs that catch certain kinds of errors.
let txn = create_transaction();
// do something with the transaction
txn.commit(); // consume the txn
Right now, you can't implement this API without choosing between either silently rolling back unless the user calls `commit()`, or panicking in the Drop impl for the transaction if the user didn't explicitly call either `commit()` or `rollback()`.
Your only current choice is to use closures, which are much less composable, because you need a variant for each flavor: infallible, fallible, async fallibe, etc.
If instead the transaction is a must-move type, you would get a compiler error if you fail to call exactly one of either commit or rollback, and particularly you would be forced to consider what happens at every exit point (early-out via `?` no longer just forgets the transaction). Very nice.
The question (one question, at least) is what to do with unwinding. If we consider every function to be able to unwind, that means you basically need to have a lot of boilerplate, and it's also not clear how to express that syntactically. Even differentiating `panic = "abort"` is not simple, as there is no language feature currently that does that.
Yes, destructuring is typically the only allowed way to get rid of linear/indestructible values. If the type has private fields, this is only possible in the same module, so commit(txn) and rollback(txn) would have to be implemented in the same module as the Transaction type.
Linear types requires significant work to incorporate into the core built-in collections and types. I've been following the work on Mojo to enable Linear type support for built-in types and collections, I don't think Rust's language semantics will allow for the same level of integration (Rust is already stable).
Yes, editions are a great mechanism. It still has its limits, especially if you want easy edition migrations. All existing Rust code assumes it can drop any type whenever it wants, and that is not something you can just change across editions. You have to be very careful with defaults if you don't want conflicts when crossing edition boundaries.
The naïve idea would be to just say that all generic parameters have an implicit `where T: Move` bound, and you have to explicitly opt out of it with `where T: ?Move`, just like with `?Sized`.
In fact, that's exactly how I would expect it to work, but there may be non-obvious drawbacks.
The compat issue has always been associated types on std traits.
For example, should Iterator::Item be Move or ?Move
If you leave it as Move, you can't create any iterators over !Move types. If you change it to ?Move, then functions using generic iterators can't assume that the elements of an Iterator are always moveable. Which is a breaking change compared to now.
The most critical trait is probably Deref. Using !Move types without `Deref::Target: ?Move` is painful, because calling any method on boxed types relies on Deref.
The people working on this are aware that it poses backcompat problems that don't have obvious solutions. They are looking into non-obvious solutions. https://lcnr.de/blog/2025/11/28/implicit-auto-traits-assoc-t... is the most up-to-date one I'm currently aware of.
Niko Matsakis (T-Lang) has since also published a post on only-bounds: https://smallcultfollowing.com/babysteps/blog/2026/06/09/onl.... Sized hierarchy, Move, and Forget/Leak all share the same fundamental compat issues, so we're all pretty motivated to solve the underlying problems in a way that enables all the other features to be built on top.
I had read that post, but IIUC it doesn't address the kind of backcompat issues that I meant to be referring to (the ones that would apply even if there was only ever going to be one new question-mark trait).
I'm very probably missing something, but as a user I would definitely expect `Iterator::Item: Move`, but then also that `&{mut} T: Move where T: ?Move`.
But yeah I can see how these bounds are somewhat viral. Thanks!
Imagine if MyTrait comes from core/std. Adding an opt-out bound like ?Sized (or ?Move) is a breaking change for any generic code that relies on Sized/Move. But you want some traits from std to be open for !Move types.
Editions are limited, there is this urban myth they can do anything regarding language evolution, but that isn't the case.
First of all they don't apply to the standard library currently, secondly the kind of automatic changes that are enabled and supported by rust fix, are limited.
None. The question is more what happens when you put a `!Move` type into, say, `Vec<T>`, because that's a collection type that regularly moves its elements to a new allocation when it grows or shrinks.
Should it be possible to construct a `Vec<T>` whose size can never change? Is there a subset of Vec's API that can be annotated with `where T: ?Move`? These are all important design questions, with the potential to break 99% of existing Rust code.
By doing the same as with `Sized`: Automatically including the `Forget` bound on generic parameters and letting methods that don't need to be able to forget them opt out. That way existing code continues to compile and existing unsafe code doesn't become unsound.
I don't understand – what's the point of not collecting mass amounts of personal identity documents and face scans and linking them to online identities?
I do the same in my toy JVM (to implement the reentrant mutex+condition variable that every Java object has), except I've got a rare deadlock somewhere because, as it turns out, writing complicated low level concurrency primitives is kinda hard :p
They're great, compared to cars. But while they have a relatively fast and cheap setup, over the long term light rail and trams are a lot cheaper to run and can coexist with foot & bike traffic easier since the rails make them very predictable.
I'd place serious concerns on the "coexist with bike traffic" thing though. Tram rails are a massive danger if you're running anything smaller than these "fatbike" wheels and have to cross them for whatever reason.
The author obviously knows that too, otherwise they wouldn't have written about it. All of these issues are just how the language works, and that's the problem.
Fun post! An alternative to using futexes to store thread queues in kernel space is to store them yourself. E.g. the parking_lot[0] Rust crate, inspired by WebKit[1], uses only one byte to store the unlocked/locked/locked_contended state, and under contention uses the address of the byte to index into a global open-addressing hash table of thread queues. You look up the object's entry, lock said entry, add the thread to the queue, unlock it, and go to sleep. Because you know that there is at most one entry per thread, you can keep the load factor very low in order to keep the mutex fast and form the thread queue out of a linked list of thread-locals. Leaking the old hash on resizing helps make resizing safe.
As a result, uncontended locks work the same as described in the blog post above; under contention, performance is similar to a futex too. But now your locks are only one byte in size, regardless of platform – while Windows allows 1-byte futexes, they're always 4 bytes on Linux and iirc Darwin doesn't quite have an equivalent api (but I might be wrong there). You also have more control over parked threads if you want to implement different fairness criteria, reliable timeouts or parking callbacks.
One drawback of this is that you can only easily use this within one process, while at least on Linux futexes can be shared between processes.
I've written a blog post[2] about using futexes to implement monitors (reëntrant mutexes with an associated condvar) in a compact way for my toy Java Virtual Machine, though I've since switched to a parking-lot-like approach.
That's not a very useful property, though. Because inter-core memory works on cache-line granularities, packing more than one lock in a cache line is a Bad Idea™. Potentially it allows you to pack more data being protected by a lock with that data... but alignment rules means that you're going to invariably end up spending 4 or 8 bytes (via a regular integer or a pointer) on that lock anyways.
That's typically not true due to the `Mutex<T>` design: the `T` gets padded to its alignment, then placed into the `struct Mutex` along with the signaling byte, and that struct is padded again before being put into the outer struct.
You can avoid this with a `parking_lot::Mutex<()>` or `parking_lot::RawMutex` guarding other contents, but then you need to use `unsafe` because the borrow checker doesn't understand what you're doing.
You could use CAS loops throughout to make your locks "less than one byte" in size, i.e. one byte, or perhaps one machine word, but using the free bits in that byte/word to store arbitrary data. (This is because a CAS loop can implement any read-modify-write operation on atomically sized data. But CAS will be somewhat slower than special-cased hardware atomics, so this is a bad idea for locks that are performance-sensitive.)
Yup, that's what I'm doing - storing the two bits needed for an object's monitor in the same word as its compressed class pointer. The pointer doesn't change over the lock's lifetime.