10 ms·
Rust project goals: Immobile types and guaranteed destructors
- suddenlybananas 2mo agoCould someone explain this to me as someone who's never touched async Rust? What kind of useful patterns would this allow for?
- aabhay 2mo agoIt 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.
- simonask 2mo agoWait, how does it actually change the recursive async story? The problem today is that the compiler-synthesized struct implementing `Future` for each async function cannot contain an instance of itself without boxing, because it would create a type of infinite size. That's a separate problem that's also hard to solve nicely, because the call tree might be deep, and deciding where to cut (using Box::pin) is non-trivial.
- simonask 2mo agoThe 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.)
- pornel 2mo agoThings you'd expect to work already. It doesn't really add anything new and flashy, but removes some annoying warts. Sync code has scoped threads that enable multi-threaded execution within a function, without having to ensure the data outlives the function call. Async can't do that while guaranteeing safety. This makes tokio::spawn awkward and annoying, and is a major source why people dislike Rust's async. Low-level async code that polls Futures requires using the Pin wrapper type, which is unergonomic, and doesn't really guarantee safety, but it's more like a "be careful here" sign. Proposed changes would make that code look more like normal Rust and work without unsafe escape hatches.
- simonask 2mo ago> and is a major source why people dislike Rust's async It's worth mentioning that there is, in fact, no language out there other than Rust that can even do this in the first place. Some languages give the illusion that they support it by boxing the stack frame of async functions and letting a garbage collector deal with the consequences, but that comes with significant drawbacks too (additional GC pressure, heap allocation overhead, requiring a GC in the first place). You can do it with C++ coroutines, but it's much harder to do correctly than in Rust if you want to maintain any sense of conviction that the system is correct. The main reason that structured async concurrency would be so awesome to have is that it feels like Rust has the right set of features that could enable it with a set of constraints that are so much more attractive than any other language out there can provide - no overhead, "just works" with no drawbacks. (For the record, you can actually get pretty far today using primitives like `FuturesUnordered` instead of `tokio::spawn` and similar, but this sidesteps the runtime's scheduler, so YMMV. This basically creates a task-local mini-scheduler for your futures, which may or may not be sufficient.)
- spacechild1 2mo agoC++26 adopted senders/receivers (std::execution) as its official concurrency model, with the explicit aim of supporting structured concurrency. See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2300r10.html https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p23...
- Tazerenix 2mo agoMore algebraic effects being retrofitted onto Rust.
- dubi_steinkek 2mo agoHow so? This feel distinct from the "algebraic effects"-like features like constness, async, can-panic, can-unwind, etc., since this is a property of the types themselves rather than of functions.
- Tazerenix 2mo agoThe traits are essentially effect handlers for effects like `drop<T>`, `move<T>`, `forget<T>` which are implicitly charged to the a function which owns a `T` and does drops, moves, or forgets it. Inferring the capabilities of the function from the traits of the types of the arguments is similar to tracking effects. The function charges `drop<T>` when `x: T` goes out of scope, which is handled by the trait implementation. If Rust had a proper algebraic effects type system, you would be able to see this directly in the signature of the function (and even more, if the trait impls themselves had their effects tracked, you'd be able to see from the signature of the function the side effects of deallocation of its owned variables, like if `drop<File>` performs `io`).
- Guvante 2mo agoI haven't seen any algebraic effects system that is that powerful The closest is linear types but even then drop isn't an effect but also a function you can call to allow not continuing the references
- Tazerenix 2mo agoYeah I should have phrased that better. The traits themselves are not the effects, they're bits of code which can have side effects. Rust doesn't track those side effects in the type system yet, but !Forget especially is the essence of that idea. If you implement it for everything owned by a function, you can basically infer that the function does not have the leak effect (which would be an effect in the effect row of a Forget trait implementstjon). If you try treat memory as an effect you gain the need for several polymorphic effect type functions drop, forget, etc which map a type to the effect row charged by its corresponding Drop, Forget impl. Rust doesn't have that type system obviously.
- OskarS 2mo agomem::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?
- skitter 2mo agoBy 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.
- stymaar 2mo ago> 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>.
- klauserc 2mo agoAn alternative way to "forget" a value is to hand it off to another thread that then loops infinitely. Could of course be plugged by saying `!Forget : !Send`, but wouldn't that preclude legitimate useful scenarios for `!Forget`?
- aw1621107 2mo ago> An alternative way to "forget" a value is to hand it off to another thread that then loops infinitely. Might be able to address that by only allowing such a handoff to a thread spawned via some scoped abstraction to ensure that progress can only be made if/when the spawned thread terminates?
- Dagonfly 2mo agoPassing ownership to another thread is not the same as forgetting/leaking. The point of !Forget is ensuring that once the owner goes out of scope the destructor must be guaranteed to run. An infinite loop is not a problem, cause the new thread will never leave its scope. Ref-cycles are a problem, cause you can create a ref-cycle. Then the program flow leaves the scope which will run the drop on all RC's but not the drop on the inner type.
- stymaar 2mo agoGreat 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.
- q3k 2mo ago> 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...
- ordu 2mo agoI don't see why it may fail to integrate. Declare Pin as !Move and... thats all? I mean, there will be issues, edge-cases because it is just how these things happen, but still I don't see any fundamental issues with continuing to use Pin.
- Georgelemental 2mo agoPin applies to the pointer, !Move applies to the pointee.
- skitter 2mo agoAlthough 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.
- simonask 2mo agoFor 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.
- ordu 2mo ago> 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?
- vlovich123 2mo agoExactly - 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/move-destruct-leak/ https://smallcultfollowing.com/babysteps/blog/2025/10/21/mov...
- yccs27 2mo agoThere'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/ 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?
- rienbdj 2mo agoThis sounds like a similar approach to OxCaml
- Ygg2 2mo ago> # 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/src/2026/move-trait.md#how-does-this-relate-to-the-pin-ergonomics-initiative https://github.com/rust-lang/rust-project-goals/blob/main/sr...
- yccs27 2mo agoAh, thanks, I didn't realize "pin ergonomics" was the Rust Project name for @withoutboats' pinned places.
- panstromek 2mo agoFor 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).
- ddevnyc 2mo agoI think it's absolutely amazing to have insight into long-term goals like this for open source projects. For one thing, it can help you plan your tech stack, and can even be a source of inspiration on what sorts of topics to learn and what sorts of research to do.
- appplication 2mo agoI second this. There’s so many OSS projects where long term plans live in private discords and maintainers act like it’s necessary to keep it all a secret
- ameliaquining 2mo agoI think this one might be at greater risk than usual of being dropped, because it's explicitly mutually exclusive with another accepted project goal (pin ergonomics): https://github.com/rust-lang/rust-project-goals/blob/main/src/2026/move-trait.md#how-does-this-relate-to-the-pin-ergonomics-initiative https://github.com/rust-lang/rust-project-goals/blob/main/sr...
- afdbcreid 2mo agoIf anything, pin ergonomics is at greater risk. People still discuss if we need both, but if we don't, then we go for this goal and not pin ergonomics, as it's more general. It (the immobile types proposal) is at a greater risk because it is a very pervasive and complicated change, and the expected semantics are not fully understood. It might be that there is no way to do this well.
- hnc99rxjlw 2mo ago[flagged]
- safercplusplus 2mo agoOk, 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/README.md#norad-pointers https://github.com/duneroadrunner/SaferCPlusPlus/blob/master... [2] https://github.com/duneroadrunner/SaferCPlusPlus/blob/master/README.md#tnoradproxypointer https://github.com/duneroadrunner/SaferCPlusPlus/blob/master...
- germandiago 2mo agoLittle 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/DIP1018.md https://github.com/dlang/DIPs/blob/master/DIPs/accepted/DIP1...
- orlp 2mo agoThis is the opposite, it is further opting out of flexibility.
- eqvinox 2mo agoI guess the point is that the concepts are needed, which is true. But the Rust way (more explicit and targeted) of dealing with these concepts seems better than how either C++ or D handle it.
- germandiago 2mo agoYes, I meant the concepts needed for low level language and tweaking when high performance is of concern.
- deleted 2mo ago[deleted]
- giancarlostoro 2mo agoD is my favorite language that I wish I could use more, but the chances of me being paid to use it are too low, I wish that job market would expand, I feel like D needs a really good alternative to Vibe.d a web framework with a well designed ORM, or a rich GUI stack out of the box. Go became massive because a production ready but simple HTTP server came out of the box, and other nice to haves that made being productive in Go a breeze from day 1.
- 2mo ago
- jerf 2mo agoWouldn't this be fairly substantially backwards incompatible?
- panstromek 2mo agoBy default yes. Part of the work is figuring out how to get around that.
- jerf 2mo agoThank you.
- deleted 2mo ago[deleted]
- boccko_ 2mo agoGive me all the liberating constraints. Get rid of panic next.
- groundzeros2015 2mo agoGuaranteed destructors is probably the most complex features ever added to C++, more than templates or move semantics.
- barrkel 2mo agoThe combination of exceptions and non-trivial {copy constructor, assignment operator, destructor} are what combine to make C++ a somewhat broken language when you try to use all the features. And also responsible for poisoning the well for exceptions as an error handling mechanism for native code. Delphi did native exceptions much better IMO.
- ameliaquining 2mo agoHow do native exceptions work in Delphi?
- barrkel 2mo agoVery similar to Windows Structured Exception Handling, which the Win32 implementation used, and also looks similar to Java, but without checked exceptions. try/except/end and try/finally/end blocks. The major differences with C++: * objects are references not values, cutting out all the copy constructor, assignment operator, destruction on out of scope etc. * objects are zero-initialized after allocation and before constructors run * constructors are run from most derived to least derived * calling Free method checks if Self is nil * this + zero init means that you can call '.Free' on all the objects you reference in the destructor without checking if they're nil first, handling partial construction It's not a memory safe language, but it does give you a bunch of idioms that, if you stick to them, you don't feel nearly as much pain as C++.
- pjmlp 2mo agoNot all objects are references, because Delphi still supports the Turbo Pascal object model for compatibility, regardless of how many years they are deprecated now. Agree with the rest, I always loved how Borland picked Object Pascal extensions from Apple, merged them with what was going on with Modula languages and C++ during the 1990's, while coming out with something saner. While at the same time keeping C++ around, as market value when buying the whole package. What everyone is going crazy regarding Zig, Odin, Jai, C3, whatever improvements over C and C++, where already available in Delphi, Modula-2, Ada and co, with Delphi being the most affordable option until Borland decided to pivot to big corp.
- dorjoycb 2mo agoDoes 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.
- ameliaquining 2mo agoThis is not currently a project goal. As with many things in the Rust project, it could be, if someone wanted to step up and dedicate the required engineering resources.
- afdbcreid 2mo agoBut also come with a plausible implementation strategy, that from what I know is not currently known for no-panic.
- ameliaquining 2mo agoOut of curiosity, is there some reason it would be harder than I'm imagining? It seems like a fairly straightforward type-system feature. (I.e., it would be a lot of work, but only because any significant new language feature is a lot of work.)
- afdbcreid 2mo agoThe problem with the trivial implementation is that it's barely useful. You cannot use indexing, for example. Not to mention it's still not easy: to have any usefulness, there will need to be a way to be generic over it (similar to keyword generics), and that's far from trivial.
- ameliaquining 2mo agoWell, yeah, I would not expect indexing (on the standard library types) to be usable within a no-panic function, because panicking on out-of-range inputs is what those operations do. Admittedly, probably lots of people would initially think "oh, being sure never to panic sounds useful", look into it more, realize what's actually involved, and decide "never mind, I'll stick with the risk of panicking" (which is the correct decision for almost all software). But there'd still be use cases for no-panic. If this is the "trivial" implementation, then I'm not sure what a "nontrivial" implementation would look like, unless it means adding a proof-tactics language to Rust to allow statically checked proofs of arbitrary program properties. Which would be really cool and all kinds of useful, but which I think everyone realizes would be a gargantuan project even to design, let alone implement. Genericity is not strictly required for no-panic to be useful (lots of type-system features still don't have it), but yes, it would be very nice to have. (Though one might hope that, once they figure out genericity for one keyword (probably const), that'd make it easier to add for others.)
- _alphageek 2mo agoIntresting. Immovability becomes a property of the type (!Move) rather than the place (Pin), and the goal is to eventually deprecate Pin outright rather than paper over it with pin ergonomics. !Forge is what finally unblocks safe scoped spawn: handle that can't be mem::forgeten has a destructor that's guaranteed to run.
- drnick1 2mo ago> Haskell is a very good language for writing this kind of application. I won't touch Pandoc with a ten foot pole because of the endless Haskell dependencies on Arch Linux. So no, Haskell is a pretty horrible choice.
- three_burgers 2mo agoWhere was Haskell mentioned?