-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Add linear type facility #776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
as argument to Drop::drop.
The "Linear" trait is the wrong way around: implementing a trait for a type should mean that type supports a greater number of operations, in this case it's the opposite. It should be the same as the "Sized" trait: the trait should be called "Affine", or "NonLinear", so that implementing the trait allows one to implicitly drop the type. Otherwise, generic methods will need to have an implicit "!Linear" bound on all of their generic types, which would be bizarre. Additionally, this removes alot of the redundant complexity from the RFC. All of the stuff about a new special kind of pointer to get around the problem of having linear types in non-linear types can be removed, just give the "NonLinear" trait a method which can make "self" non-linear type just before drop, then types can explicitly implement "NonLinear" even if they contain linear fields, while the compiler will automatically implement "NonLinear" for all types which don't contain linear fields. |
I’ve always liked the idea (which I’ve seen in a few places by different people) of using |
@Diggsey I think the new pointer type is necessary whether the type-trait signifies "Linear" (removing a capability from an Affine var) or "Affine" (adding a capability to a Linear var). In this design, the linear property of a variable is viral: any For what it's worth, the proposed change to |
@Diggsey and @P1start are correct. To do this elegantly/properly, linearity needs to be the default. This is why it is very important that this be done before 1.0. Treating the new @aidancully I think the solution to get the affine fields out is: let (mut first_affine_field, _) = linear_val;
do_something_with(&mut first_affine_field); The rules are simple: you can can deconstruct a linear type in this way iff all variants and fields are public. [If https://github.com//pull/736 which adapts the some rule, all the better for consistency and mutual-intuitiveness.] I've mentioned it before, but I like |
@Ericson2314 The problem I was having wasn't getting the affine fields out of a linear variable - that part is fine, [1]: In this design, it's always inappropriate to silently drop the linear portion of a linear value. The major intent behind linear values is to allow a library author to define the ways in which it is safe to dispose of one. In your example, it appears that the linear portion of a compound value is implicitly dropped by some mechanism that the library author cannot control. This would violate the intention behind the proposal... // try to define an Affine variable that contains a Linear field by having
// the drop hook use a specific strategy for cleaning up the linear field:
struct AffineOfLinear(MakeLinear);
impl Drop for AffineOfLinear {
fn drop(&mut self) {
// the following cannot compile, because partial moves are disallowed from &mut pointers.
self.0.consume();
// but moves are also the only "safe" way of consuming Linear instances. (we explicitly
// do not want a MakeLinear instance to be automatically consumed during the
// drop hook.) So how do we consume the MakeLinear field? In the proposal, I
// added a new DropPtr<T> type to allow partial moves from `self` during drop.
}
}
// intention is to support:
{
let x = AffineOfLinear(MakeLinear);
// implicit drops are allowed on AffineOfLinear types.
// x's drop hook will be called when the scope closes.
}
// we want to force that x's drop hook explicitly cleans up after its linear field. I'm amenable to inverting the meaning of the type-trait so that |
I experimented with removing the |
@aidancully Perhaps the marker should be called |
I'd just like to point out that we really need this for gfx-rs/gfx#288 and thank @aidancully for consistently pushing and developing linear types feature. As for the ways to implement it, I'd prefer having minimal complexity over backwards compatibility, because we are still in alpha. |
I do feel that this PR introduces way too much complexity though, at least for our simple needs. The way we need linear types to work is basically just have their automatic destruction forbidden. Thus, simply disallow Our case in gfx-rs just needs to enforce (at compile time) a specific spot for resource destruction, that's it. I believe this could be achieved with less intrusion to the language. For (an oversimplified) example, we could just have a Please note that I'm new to compiler design and realize that my understanding may be to flawed and simplified. |
What's the point of keeping a |
I don't see what unwinding semantics have to do with the signature of |
I'll have a bit more to say, here, but for what it's worth: An earlier design had It's probably possible to change the drop behavior inside the drop hook itself... but it'd be weird if the same type ( |
First of all, as I think we agree, the sneaky infinite loop is not a soundness problem, just an ergonomic problem: If panic!() inhabits all types, infinite recursion with moving should consume all types. |
@aidancully Are you on IRC (#rust)? I'd like to ask a few questions to get a better picture... |
My solution starts with disabling custom drops everywhere. Period. At this point, let's review the situation:
Room for improvement, but not the worst ergonomic fallout? Right?! |
How do we improve this ergonomic story? Finer grained implicits and warnings for all four traits I see potentially four categories:
The larger point is that currently both |
If annotating a type to show its membership in those four categories globally is too course, it different scopes can override the default per scope. The biggest use cases I can think of are:
These finer-grained granularity can be added backwards comparably, and I don't think users will implement types with custom destructors as ubiquitous and innocuous as |
The one exception to the coppying-destroying symmetry is that unwinding would want to call destroy if Destroy was implemented. So unfortunately while Destory is "denotationally" a normal trait, it would need special backend (unwinding) and frontend (implicit->explicit desugaring) support. |
I'd like to move the drop discussion to another RFC, so that the smaller piece can be considered independently, and possibly accepted for 1.0 without requiring the whole rest of this proposal to be accepted. I've started a thread on internals for discussion of this narrow point, maybe the design can be resolved there? |
Regarding the "complexity" concerns, I |
@aidancully The summary makes sense to me now that I got a better picture of it. It doesn't seems to add more complexity than necessary, considering the alternative might be to make all types linear by default to be solved properly. I understand the drop problems must be dealt with one way or another, but I don't think this will hurt ergonomics. +1 Assuming this works, and I don't know whether some minor things could be improved. I think that supporting this way or through similar functionality would solve the problems we have. |
postponing for some point after 1.0. |
(See issue #814) |
@pnkfelix Given the extensive discussion of backwards (in)compatibility issues in the RFC and comments, I think the postponement might have deserved some justification and those aspects some mention in it. |
@glaebhoerl the team discussed briefly, but its possible we overlooked backwards compatibility issues that would make postponement untenable. I will review the RFC and comment thread, and try to extract out a high-level summary of what we would need to do now to make doing this feasible later. |
@Ericson2314 I think your text above is massive because you did not include a newline between your paragraph and the (I am going to take the liberty of editing your comment to fix the formatting.) |
@glaebhoerl okay, so obviously the I had been musing about ways we might try to keep the current API while adding other destructor-enabling traits that would provide alternative signatures. But even if that were a palatable option (and it may well not be), one would still want all of the various destructor signatures to be composable, and that sounds like a harder nut to crack. So, probably better to either put in forward-compatible changes now, or decide up front that we are not going to be supporting this kind of feature without deeper changes to the language and/or APIs of Rust 1.0. |
(I do appreciate @aidancully 's posting of http://internals.rust-lang.org/t/moves-from-self-during-the-drop-hook/1536, which seems to factor out the main backwards-compatibility concern. So I plan to focus my attention on that, i.e. evaluating the options given in the thread there and trying to figure out whether we can adopt any of them.) |
I don't have a very clear understanding of the issues myself, just wanted to bring some attention to them. Thanks! |
@glaebhoerl Thanks for bringing up the backwards computability issues. @pnkfelix thanks for double checking the back-compat situation (and fixing my markdown error). |
Questions on what, why, how researchy, and criteria for adding features to Rust. (1) Can you please explain the idea to ordinary software engineers who aren't math majors? Web searches turn up more info that I don't understand and/or don't seem to fit, like "Use-Once" Variables on c2 and Substructural type system on Wikipedia. "Used exactly once" is not about Does "like sprinkling free() calls" mean the idea is to allow explicitly freeing resources in a non-LIFO order with the compiler checking that we didn't forget? (2) Why is this compelling? In the first example, "Force explicit mutex drop", the everyday way to move code outside the mutex is to move it outside that RAII block. That may require additional temporary state but that's life with locks. Notably, avoiding deadlocks requires a globally consistent lock order and releasing locks in LIFO order. So enabling a different unlock order is ungood. (If we don't all switch from locking shared state to sending messages between threads, a lock-checker like Spin would be nice.) The other example says "Now we must manually drop(data); and drop(r) here, othewise check will segfault. because data will already be dropped." How so? We must manually (3) Is this a research feature or a proven production feature? To quote the Java Language Spec
Contrast Java with Scala -- a research language to rapidly try ideas. Of course some ideas don't pan out (like implicits). Rust has one big research experiment (ownership/borrowing). If it aims squarely to be a production language, it ought to hinge on that one and do other experiments in forks. (4) Can this be simplified and made approachable, akin to the scope idea? (5) Does it really have to be built into the type system? Is it better as a property of variables than a property of types? (6) Can this be implemented as a compiler plug-in or a separate code analysis tool like the Spin model checker and the SPARK 2014 validation tools? (7) What are the criteria for adding features to Rust? I suggest factors like high bang/buck, demonstrated broad utility and usability for Rust's target uses, worth the total costs & opportunity costs to the Rust ecosystem, not implementable in a library, and minimalist. |
@1fish2 The idea is to allow more strict control of how objects are used in a library interface. An example from nature: Some quantities in a physics are preserved and should neither be duplicated or destroyed without being counter balanced with something else. Linear types gives you a tool to build APIs with this property. For example, when you are dealing with references to external resources, such as textures in GPU memory, which will cause memory leaks if not destroyed through a device object. It can also be used for transforming messages that keeps information that should never be lost. Because you can enforce this at compile time, you can make safer zero cost abstractions. It puts a constraint on the number of valid programs, and therefore must be part of the type system. |
@1fish2 wrote:
I think the example is trying to describe a problem with eager-drop semantics. If we had eager-drop semantics, then the explicit |
@1fish2 @pnkfelix Yes, I believed this RFC addressed the major shortcoming in eager drop, in that eager drop didn't interact well with FFI on its own, but with linear types, it could be made to do so. I had hoped to see eager drop adopted in the language before 1.0, but since that isn't happening, this part of the RFC can be removed. But that's certainly not the only reason I wanted the feature. I don't go into this in the RFC because it's a bigger lift, but I want to support intrusive structures, in which ownership of a containing structure can be moved via ownership of a field. If you do this kind of thing, then dropping the field-pointer is a bug: you need to get back to the container before you have something that can be dropped. So I wanted a linear I also wanted to support more advanced release'ing facilities, so that a client is forced to make a decision about how a variable should be released. For example, I've got a message-passing architecture in which a request-message is sent to a receiver, the receiver handles the request, then returns the request back to its originator, but with the message modified to indicate success or failure status of the operation. With linear types, I could force the I currently have both of these ideas implemented in C and C++ in a several-hundred-KLOC embedded application at my day job. I have to simulate linearity because I'm using C and C++ (by using panics when linearity constraints might be violated), but the ideas have (IMO) worked very well for us. I went to the effort of producing the RFC for two main reasons:
Of course, I'm not the only one interested in the facility, but those were my reasons. |
@1fish2 @pnkfelix @aidancully Related to intrusive data structures is that certain idioms of fine-grained locking are currently not possible to express/enforce in Rust. struct Node<T> { payload: T, children: [Option<Box<SomeLock<Node<T>>>>; 2] } Consider a tree with lock per node. To traverse the tree one locks a node, locks its child, then unlocks the parent (hand-over-hand locking I think this is sometimes called?). To remove a node one must have it locked (to ensure that nobody else has access to it) and have its parents locked (to ensure nobody else is trying to lock a lock that is about to be destroyed). struct Node<T> { payload: T, children: [SomeLock<Option<Box<Node<T>>>>; 2] } Well, first of all, the locking of the parents---and the problems it is supposed to solve--are a big jank red flag. The pointer to the node represents a capability to the node, and seeing that this is going to be tree and not the dag, might as well lock the pointer and not the node. This also can help avoid "dummy head nodes". Then just lock the owning pointer to remove the node -- simple as that. The rest of this comment is going to assume the pointers are locked instead of nodes because this seems less jank and less hopelessly incompatible with Rust, but I make no claims that doing this isn't somehow less performant or otherwise doesn't introduce other complications. Assuming that works and has no performance problems or other downside (which I am not at all sure about), there is still a still a problem with just traversing the tree. Rust insists that one borrow a lock to lock it (which, don't get me wrong, is a normally a good idea). This means the to access a node, one must borrow its lock, which is owned by its parents so borrow that lock... all the way back to the root. This means that the tree is globally locked after all! A big reason for this is that locking a node gives one power over not just it, but subtree of it and nodes reachable by it, allowing one to all or large parts of that subtree en masse without further locking. The borrows ensure nobody else gets the locks, and therefore nobody can drop the subtree. [Perhaps I don't think there is any hope of getting Rust to understand this idiom "natively" (like it understands borrowing), but it would be nice to be able to encapsulate this idiom once so data structures and algorithms can be ensured to respect it with the type system. I don't have an exact plan for how this is done, but a key ingredient is to fake the "shallow" ownership the hand over hand locking uses in order to ensure the children of the node cannot be dropped with non-aliasable reference to the node ( Hopefully that makes some sense. |
Rendered