Skip to content

RFC: Add linear type facility #814

Description

@pnkfelix

Rendered

Tracking issue for postponed PR #776

Activity

  1. added
    postponedRFCs that have been postponed and may be revisited at a later time.
    on Feb 5, 2015
  2. mzabaluev commented on Feb 27, 2015

    @mzabaluev
    Contributor

    This may be useful to support close methods for I/O objects that need error reporting when being disposed of. See a discussion thread on I/O issues.

  3. Munksgaard commented on Sep 15, 2015

    @Munksgaard

    Are there any plans for when we take this up again?

    #523 seems to be related, by the way.

  4. aidancully commented on Sep 15, 2015

    @aidancully

    @Munksgaard I've factored some of this RFC out into #1180, and (opened a discussion on internals about another part)[https://internals.rust-lang.org/t/pre-rfc-excdrop-trait-for-different-drop-glue-during-unwinding/2242]. I've been working through an implementation of #1180, but real life is taking too much time to make any progress at all for probably the next month or so.

  5. Rufflewind commented on Apr 17, 2017

    @Rufflewind

    Aside from fallible Drop constructors (e.g. closing a file), there’s another situation where it could come in handy: Sometimes an resource is held by another thread/process/host and dropping would require waiting for for the other side to complete whatever they are doing. Implicit drops would cause cause the program to block, possibly unintentionally (which can further lead to unintended deadlocks). Worse, the protocol might not even offer a way to wait for it, in which case there’s nothing you can do but panic. In these kinds of cases, it would have been better to have simply caught it as a compile error.

    I think linear types are suited for transient, active things that need to be dealt with carefully and explicitly, whereas affine types are more like passive objects that could be thrown away without regard. Which one is more suitable for a particular resource is probably a bit subjective and dependent on the robustness and reliability demands.

    It might even be possible to have a rudimentary lint-based implementation of linear types: if an object gets dropped and the destructor unconditionally panics, then that’s almost certainly a bug and should be warned.

  6. jethrogb commented on Apr 17, 2017

    @jethrogb
    Contributor

    It might even be possible to have a rudimentary lint-based implementation of linear types: if an object gets dropped and the destructor unconditionally panics, then that’s almost certainly a bug and should be warned.

    How would that work? Presumably when people say linear types they don't mean types with values with an infinite lifetime, but rather that you need to pass them to some special destroy function by value when you're done with them. If you have a Drop impl that always panics you can't even do this (safely).

  7. Rufflewind commented on Apr 17, 2017

    @Rufflewind

    How would that work? Presumably when people say linear types they don't mean types with values with an infinite lifetime, but rather that you need to pass them to some special destroy function by value when you're done with them.

    It’s not infinitely long, but the time at which it becomes destructible is not statically known, and may require the user to take certain actions before it becomes destructible.

    If you have a Drop impl that always panics you can't even do this (safely).

    Yes, panicking in drop is really awkward, so I’m hoping linear types could help obviate the need for such workarounds.

  8. added
    T-langRelevant to the language team, which will review and decide on the RFC.
    on May 13, 2017
  9. carado commented on Apr 25, 2021

    @carado

    6 years now, is this ever gonna be considered ?
    There have been a bunch of times when I've wanted something like this.

  10. Philonoist commented on Jan 24, 2022

    @Philonoist

    This is so important! If this would be supported, rust could be the primary language to compile to cairo bytecode...

  11. leviska commented on Apr 14, 2022

    @leviska

    Use cases:

    1. Async drop. Instead of implementing async drop, you can forbid drop for the type and implement async fn method that must be called instead of dropping.
    2. Drop with arguments. Imagine a handle, that is created from some resource manager and needs a reference to this manager on drop. One way is to store the reference inside the handle (which can be tricky/expensive with Arcs/Mutexes/etc.), but the other way is to make drop accept the reference to the resource manager. With this you can forbid drop and implement custom function that accepts additional arguments.
    3. Drop that can fail/can have side effects. Again, the same idea, you can forbid drop and write a function, which will return error, for example, if drop failed, so it will be the job of the programmer to handle this error correctly

    You can already write this functions and use them, but I think the biggest problem is that if you forget to call them, the compiler will place the default drop (and drops can be hidden in some function calls). And I understand that there is a big problem with handling this custom drops during panic, so may be add cheaty linear type:

    Basically instead of just banning the drop, may be add an 'annotation' to the compiler, saying "do not add drop for this type during compilation; instead, throw compile error". So, an example:

    {
        let a = Foo::new();
        // compiler 'will add' std::mem::drop(a);
    }

    Instead, if the type is annotated as "no drop", in this snippet compiler will just throw an error. This can help with cases 1 and 3: in async case, we can block in the drop; in the drop-can-fail case, we could just ignore an error (because it will be called only during panic and something already went wrong). And even in the second case some one can be fine with resources "leaking" from resource manager during panic.

    I know this is an error prone way to do this, but it kinda doesn't violate rust rules. You can already write all that code (blocking, ignore an error, do nothing with custom resource manager) in safe rust, but yes, you shouldn't, and the lack of the no-drop kinda forces you to find a better way. But I think this "annotation" could help in a lot of cases, where there is just no better solution.

  12. Demindiro commented on Dec 25, 2022

    @Demindiro

    I've found that using must_use in combination with forbid(unused_must_use) is effective as a crappy form of linear types, e.g.:

    #![forbid(unused_must_use)]
    
    #[derive(Debug)]
    #[must_use = "Must be manually destroyed with `T::destroy()`"]
    struct T;
    
    impl T {
        fn destroy(self) {
            std::mem::forget(self)
        }
    }
    
    impl Drop for T {
        fn drop(&mut self) {
            if !std::thread::panicking() {
                panic!("Must be destroyed with `T::destroy()`");
            }
        }
    }
    
    fn main() {
        dbg!(T);
    }
    error: unused `T` that must be used
      --> src/main.rs:22:5
       |
    22 |     dbg!(T);
       |     ^^^^^^^
       |
       = note: Must be manually destroyed with `T::destroy()`
    

    If you replace dbg!(T) with dbg!(T).destroy() it compiles and works as expected.

    EDIT: in hindsight it doesn't work if you have e.g. f(&self) and call T.f(). Ah well

  13. SOF3 commented on Dec 29, 2022

    @SOF3

    Where does the word "linear" come from? Is there any prior work in other languages for reference?

  14. Kixiron commented on Dec 29, 2022

    @Kixiron

    Linear types are a form of substructural type systems, notable implementors are Clean and Haskell (via GHC extension)

  15. afetisov commented on Dec 29, 2022

    @afetisov

    @SOF3 The word "linear" comes from the notion of linear logic. I believe the concept, and the terminology, were first proposed in a 1987 paper Linear Logic by J.-Y. Girard. The terminology was likely motivated by two related concepts, coming from the theory of vector spaces and linear operators. People often talk about "linear relations" or "linear terms" when variables occur only once in the formula, by analogy with linear functions, which have a single occurrence of each coordinate variable in their expression. In linear logic, an element of each type can be used only once in the formula, because construction of pairs (x1, x2): X \times X is forbidden for general types. Linear logic doesn't allow duplication of values (unless special operators are used). Similarly, linear logic doesn't allow silent discarding of values: if you have an element x: X, you must explicitly thread it through all formulas, and can't just "forget" to use it.

    The second, more conceptual, reason to call this logic "linear" is that it arises as internal logic of the additive symmetric monoidal category of vector spaces. Basically it is the only way your logic could be structured if vector spaces were all you knew about, and it allows to prove all facts about them. The categorical model served as the guiding star for the formulation of logic, but for practical applications the later found model of resource semantics is much more relevant.

  16. aleokdev commented on Feb 16, 2023

    @aleokdev

    Any progress on this? What is it blocked on? This would be incredibly useful for interfacing with externally handled buffers, such as the ones present in a GPU. Other alternatives require having a device around to be able to destroy the resources, which unnecessarily adds bloat to the structure or to the global namespace.

    I'd be willing to work on it if the PR gets considered.

  17. golddranks commented on Feb 16, 2023

    @golddranks

    @aleokdev Unlikely to happen. I think @Gankra's blog post "The Pain Of Real Linear Types in Rust", that for some reason is not linked in this thread, killed the feature for many: https://faultlore.com/blah/linear-rust/

  18. psionic-k commented on May 2, 2026

    @psionic-k

    I will argue that we are overemphasizing the cost of an overly ambitious implementation when the real implementation we need supports almost nothing but prevents the very real problems of in-flight things being silently dropped. The contract we aim to create is not an unreasonable one, that no envelope may be licked unless it is sealed and sent.

    https://faultlore.com/blah/linear-rust/
    There would need to be some special handling if <T: Copy>

    It may not be a contradiction to split the licked envelope so long as both parts are eventually sealed and sent. However, if the semantics of MustConsume tend to involve some external service holding an imperiled LickedEnvelope, isn't it more natural to expect the user to tell the external service to split the real external envelope before requiring the program to express that both parts of its representation will be consumed? If so, isn't it natural for the compiler to require Clone and Copy to be manually implemented for any MustConsume since the user will need to notify the external service before any two such imperiled LickedEnvelopes can be made to exist? Wouldn't the compiler's refusal to implement MustConsume and Copy automatically be a strong suggestion to the user that these two ideas are orthogonal, and isn't that suggestion accurate?

    A bunch of the standard library will need to be “upgraded” to have ?Leave support.

    I don't buy this. I would be fine with all of std disallowing MustConsume the same way many types disallow all but Sized. It is easy enough to hack away MustConsume temporarily with some other outer MustConsume contract on a container that uses Vec<MustConsumeErased> internally. If I'm writing APIs for the user, this maneuver is merely some harmless boilerplate that a derive crate would swiftly conceal while maintaining soundness.

    I think there's a real case that we're tunnel-visioning on the hard implications that are not meaningful to actual use cases for MustConsume while ignoring the real harm to users when imperiled LickedEnvelopes are silently dropped or only explode at runtime in the rare paths that were meant to recover. Like thread locals or branded lifetimes, MustConsume is the kind of semantic that I want automation for when the flow control is naturally going to constrain the problem to a much smaller surface to defend.

    People who say they want Linear™ Types In Rust actually just want Proper Support For Relevant™ Types.

    In the case of a panic, I may still want to implement custom Drop behavior that may clean up a runtime condition my program was not allowed to express, enabling imperiled LickedEnvelope to be rescued on the remote service. If this counts as using my type at-least-once (once for the MustConsume that may have failed and once in a panic hook) then I would consider Relevant types sufficient.

    This would be incredibly useful for interfacing with externally handled buffers, such as the ones present in a GPU.

    I have encountered this need when dealing with this same need for a contract. In stable Rust, I can warn and I can explode for the user at runtime, and neither one of these things are correct. When we swear to do it all right in the name of correctness, let us not forget the errors that already happen and the robustness of conservative solutions that only support the vary narrow case.

    What I'm looking at in the meantime is this perhaps dirty thing. If it saves my users from writing programs that drop an already recorded command buffer and no other implementation would demand any different semantics on the public API, that's a win.

  19. Hisanatos commented on Jul 20, 2026

    @Hisanatos

    Out of pure curiosity, because I'm just a normal programmer usually using some programming language, and not fully understanding why it works or how.
    Wasn't this accepted here https://github.com/rust-lang/rust-project-goals/blob/main/src/2026/move-trait.md as a rust goal and isn't it being tracked here? rust-lang/goals#635

    As far as I understand linear types differs from Rust implementation in this way
    Rust: At most once
    Linear: Exactly once.

    Wouldn't auto !Forget trait solve this problem?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-langRelevant to the language team, which will review and decide on the RFC.postponedRFCs that have been postponed and may be revisited at a later time.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions