Hacker News new | past | comments | ask | show | jobs | submit

Rust project goals: Immobile types and guaranteed destructors

https://github.com/rust-lang/rust-project-goals/blob/main/src/2026/move-trait.md
For everybody who doesn't have the context, just note that this is not an accepted langauge change. It's a just project goal, which means it's accepted as something people will work on, but the design might change significantly or it can even be abandoned completely (which is pretty unlikely for this one, to be fair).
loading story #49156434
Great new! Since 2016 or so it became apparent that immovable types were a crucial missing part of Rust, but for a long time it was believed it wouldn't be possible to add them without breaking everything, which is why we ended up with the Pin hack.

I'm very glad they found a way to add it eventually, as it's really filling a glaring hole in the language.

> I'm very glad they found a way to add it eventually

Will this integrate with existing code that uses Pin<T>? If not this will split the ecosystem even further...

loading story #49153936
There's a different proposal by @withoutboats to make immovability a property of the place/reference instead of the type:

https://without.boats/blog/pinned-places/

Does this project goal mean that the rust maintainers have decided to implement @yoshuawuyts' immovable types proposal in favor of pinned places?

    > # How does this relate to the "pin ergonomics" initiative?
    > This work is an alternative to Project Goal 2025H2: Continue Experimentation with Pin Ergonomics, which includes the following extensions: 

    > A new item family pin in lvalues, e.g. &pin x, &pin mut x, &pin const x.
    > A one-off overload of Rust's Drop trait, e.g. fn drop(&pin mut self).
    > A new item kind pin in patterns, e.g. &pin <pat>.

    > Notably, this work does not solve pin's duplicate definition problem, meaning that even with these extentions we still end up with Trait and PinnedTrait variants of existing traits. The Drop trait being the exception to this, since the initiative is proposing to special-case it using a one-off overload.
https://github.com/rust-lang/rust-project-goals/blob/main/sr...
loading story #49154263
This sounds like a similar approach to OxCaml
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.

    start_transaction_async(async || { /* ... */ TransactionResult::Commit });
    start_transaction_async_try(async || { /* ... */ Ok(TransactionResult::Commit });
Ick.

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.

> 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

Can you elaborate how it may work? I mean if I create a function:

fn fail_silently(txn: Transaction) {}

then the calling code would pass the compiler, but this function presumably isn't, ok. But what can make these functions to pass:

impl Transaction { pub fn commit(self) { ... } pub fn rollback(self) { ... } }

Would you need to destructure self or what?

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.
Exactly - fail_silently is illegal and you have to actually destructure the type to explicitly implement the destructor

> How would you handle destructors with arguments?

https://smallcultfollowing.com/babysteps/blog/2025/10/21/mov...

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).
But Rust has editions.

That is a big lever language designers can use if they painted themselves into a corner.

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.
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!

Here is an example of the problem: https://play.rust-lang.org/?version=stable&mode=debug&editio...

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.

Which stdlib collections and types should become `!Move`?
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.

We already have a Vec<T> whose size can't change, it is called Box<[T]>. So Vec doesn't need to support !Move types.
Guaranteed destructors is probably the most complex features ever added to C++, more than templates or move semantics.
loading story #49157459
More algebraic effects being retrofitted onto Rust.
loading story #49158173
loading story #49154454
Does anyone know if there is any plan for no-panic to be a language feature? I think there was some discussion about this regarding Rust in the Linux kernel or embedded Rust but I don't know if there is any consensus or any plan regarding this.
mem::forget isn’t the only way you can safely leak a value, you can do it with reference cycles too, right? And there is no way for the compiler to detect that?

Isn’t that why mem::forget is safe, because you can always implement it yourself safely? How do you get around that?

loading story #49157581
> mem::forget isn’t the only way you can safely leak a value, you can do it with reference cycles too, right? And there is no way for the compiler to detect that?

But there's an easy solution for that: you make the reference-counted smart pointers require their pointee type to be Forget. It will be like how Arc<T> doesn't implement Send unless <T: Sync>.

loading story #49155308
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.
Ok, I guess somebody has to provide the youngsters/uninitiated with some context. What is going on here can be viewed as part of a process of Rust (potentially) incrementally adopting the C++ model, because the Rust model is limited in important ways.

Specifically, Rust's "necessarily-trivial-destructive" moves make it possible for a memory location previously holding a valid object to become invalid without a destructor (or any other handler) being called. Accommodating this possibility resulted in unforeseen (by many) limitations, particularly in the safe subset. (See "the leakpocalypse".) This was partially addressed by the introduction of "pinning" into the Rust language. The posted github page suggests that this sort of pinning is not the ideal approach, and that it is more effective to make the "unmovability" of an object a property of the object's type, rather than a property of the reference to the object, as is the case with the pinning approach.

To be clear, we're talking about Rust-style "necessarily-trivial-destructive" movability here. Traditionally, C++ doesn't really support this sort of movability. That is, even if an object's contents are ("conceptually") moved to a different location, the original source object remains (at its original location) until it is otherwise destroyed (and its destructor called). So in C++, all types are "immovable" in the sense of the posted github page.

The github page notes how these "immovable" types can support self-references completely in the safe subset in a way that pinning can't.

> This unblocks patterns that are currently impossible in safe Rust.

For an idea of some other unblocked patterns, you can consider so-called "norad" pointers [1] (and proxy pointers [2]) in the SaferCPlusPlus library. Analogous to how `RefCell` references can be used to express references that cannot be statically verified to conform to Rust's "aliasing-xor-mutability" restrictions, "norad" pointers can be used to express references that cannot be statically verified to be lifetime safe. This would include, for example, all manner of cyclic references beyond just "self-references".

I think you could implement a version of these norad pointers in Rust that can safely target these immovable types (whose destructor is guaranteed to be called while the object is still in its original location). But note that the C++ implementation uses static inheritance (which Rust does not support) to avoid the noise having to access the target object as "interior" content (like with `RefCell`s).

With the availability of these flexible references, one could imagine immovable types becoming popular in things like games / entity component systems, GUI frameworks, browser engines, and any place where "back pointers" would be convenient. One might even imagine that at some point, types being "immovable" could become the popular default for object types in Rust (among biological and/or non-biological Rust programmers). At which point, people may decide that actually they do want (the contents of) some of their immovable types to be "movable", but they don't necessarily need the object to be destructively movable. So you could imagine the introduction of standard `nondestructive_move()` (and `nondestructive_move_from()`) methods that would be companions of the existing `clone()` (and `clone_from()`) methods. At which point Rust would have counterparts for C++ copy and move constructors (and assignment operators).

In my view, this adoption of the C++ model (potentially) addresses Rust's main limitation. With one consequence being to potentially make automated translation of C and C++ code to (reasonable code in) the safe subset of Rust much more feasible than seems to be currently.

[1] https://github.com/duneroadrunner/SaferCPlusPlus/blob/master...

[2] https://github.com/duneroadrunner/SaferCPlusPlus/blob/master...

loading story #49155177
Give me all the liberating constraints. Get rid of panic next.
{"deleted":true,"id":49155627,"parent":49152023,"time":1785764065,"type":"comment"}
Wouldn't this be fairly substantially backwards incompatible?
loading story #49155570
Could someone explain this to me as someone who's never touched async Rust? What kind of useful patterns would this allow for?
The big one is scoped tasks, or structured concurrency.

Currently, Rust has scoped threads: Threads that are guaranteed to terminate before the function that spawned them returns. This is powerful because it allows you to pass references to data that lives on your own stack to threads that you spawn, without any bookkeeping or synchronization mechanism - just the normal borrow checker rules.

For example, you can allocate a large array, then split it into multiple non-overlapping slices, and then have a group of threads populate each slice, all in safe Rust code.

But the same isn't true for async tasks in Rust, because futures are just objects representing a state machine, and they don't get any special treatment. In particular, they carry no guarantee that the state machine will actually run to completion, which is fundamentally different from how functions run (stack frames are guaranteed to unwind in some way, either by returning or panicking, unless the entire program has terminated).

To make the situation worse, there are many cases where Rust futures are much more prone to cancellation than synchronous code, because that is also one of the big benefits of using async in the first place - for example, you may be running multiple futures in parallel, pick the result from the one that finishes first, and then cancel the rest.

Getting this stuff under control is why people say that "async cancellation" is a difficult problem to solve, and that is true in all languages that have async. These traits will hopefully make it much easier to work with in Rust.

(There are also many other interesting things you could do with this, unrelated to async. Immovable and unforgettable are both interesting properties of an object that could be used to design many cool APIs in general.)

loading story #49154282
It makes it easier to write recursive async functions. It makes it easier for async functions to borrow rather than clone from their outer scope.

All really awesome, non controversial and ergonomic things.

loading story #49155008
Little by little, Rust, like D, acknowledges that C++ flexibility regarding object construction, copy and move, even if too much as a default, is sometimes needed :)

I saw in D years ago how they also checked into this flexibility after getting some use cases for it (in this case, copying): https://github.com/dlang/DIPs/blob/master/DIPs/accepted/DIP1...

loading story #49155349
loading story #49155852
loading story #49156040