Traits #73
Replies: 8 comments 6 replies
-
|
I like the overall approach here. Something that I'm trying to wrap my head around is what this means and what impact this has for those trait in rust that are used to control specific language features (
impl<T> Index<usize, Output=T> for Vec<T> {
fn index(&self, idx: usize) -> &T {
requires(idx < self.len());
ensures(|res: &T| self.view().index(idx) == res);
...
}
}and similarly, but with a more complicated spec for impl<T> IndexMut<usize, Output=T> for Vec<T> {
fn index_mut<'a>(&mut self, idx: usize) -> &'a mut T {
requires(idx < self.len());
ensures(|res: &mut T| old(self).view().index(idx) == res);
// and, hypothetically,
after<'a>(|ref: &mut T| {
ensures(self.view() == old(self).view().set(idx, *ref))
})
}
}Of course the design for traits may influence decisions on returning mutable references, or viceversa, depending on what ends up making the most sense in general. For this reason, I was considering whether it would make sense to consider an approach where, for some traits (mostly those that control language features), one does not provide requires/ensures on the trait definition, but they only provide one for each implementation (in the Now, it's entirely possible these trait can also be defined and implemented using the approach Chris describes, but I'm not clear about the strategy to refer to spec functions for the requires/ensures. Do we imagine we will redefine Are we concerned about the existing implementations of these traits in the standard library? We need to make sure these implementors may inherit trait requires/ensures. To be clear, I like the fact that Chris' proposal is very consistent, and I'm trying to figure out how it would handle cases where traits could use a tailored requires/ensures. I think what I'm trying to tease apart here is that I think traits have two uses in rust:
I'm wondering if these two uses warrant two different interactions with the verifier. |
Beta Was this translation helpful? Give feedback.
-
|
I think Chris's suggestion for having a 'spec' method that is used by the requires/ensures make sense. If I understand it correctly, the suggestion is that Like Andrea, I was too was wondering about how exactly we would "redefine"
|
Beta Was this translation helpful? Give feedback.
-
|
We don't yet have a way to add extra spec functions to existing Rust types/traits. I'm assuming that we'll eventually want this for other reasons, such as adding a |
Beta Was this translation helpful? Give feedback.
-
|
The static dispatch for We can start (as it may already be the case now) by special-casing
Yep. (This is the part of this feature that isn't quite complete, I need to map |
Beta Was this translation helpful? Give feedback.
-
|
Again, I generally think the design Chris is proposing is elegant and should cover the majority of uses. Another thing that came to mind (but I haven't thought about it carefully yet) is how it interacts with potential blanket trait implementations, e.g. or, e.g. We could obviously disallow these, to start. |
Beta Was this translation helpful? Give feedback.
-
|
One other related topic: we have to ensure that proof/spec trait methods terminate. It's very easy to write nonterminating trait methods in Rust. Here's an example infinite loop: Here's another infinite loop: Coq and F* support type classes while still ensuring termination. I propose to follow their approach. (See https://coq.inria.fr/refman/addendum/type-classes.html for more information.) The key idea is that in Coq/F*, traits / type classes are just built out of ordinary datatypes and function types, so the rules for termination for datatypes and function types ensure that traits / type classes also terminate. A trait with methods m1...mn is encoded as a "dictionary" datatype containing fields f1...fn, where each field is a function type. Implementations of the trait are instantiations of the dictionary datatype with particular function values for the fields f1...fn. Trait bounds like So the termination criteria I'm proposing is to check whether a Rust program could in principle be translated into Coq/F*-style type classes. (We wouldn't actually have to do this translation -- we'd just have to check that it would be possible without causing a cycle in the datatype declarations and datatype instantiations.) In the first example above, the call to In the second example above, the trait definition |
Beta Was this translation helpful? Give feedback.
-
|
There's a discussion on Dafny's approach at dafny-lang/dafny#1588 . At the moment, there's a "draconian restriction [that] outlaws the useful case where a trait is placed in a library". Dafny traits are a little different since they are more like Java/C# interfaces than type classes, so they might be looking at a different point in the design space. |
Beta Was this translation helpful? Give feedback.
-
|
I believe all of what's been discussed here as been implemented (mainly by @Chris-Hawblitzel). @Chris-Hawblitzel can I go ahead and close this? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
When we support traits, we'll need:
We can put the trait method requires/ensures in a dummy default method:
When the ghost code eraser sees
no_method_body(), it will erase the method body, leavingfn foo(&self);.Trait method implementations inherit the requires/ensures of the trait method declarations. It is sound to allow implementations to weaken the requires or strengthen the ensures. We can consider various policies for how this inheritance works:
inherited_requires || additional_requires) and "and" the ensures (inherited_ensures && additional_ensures).We could also have some combination of 1, 2, and 3.
Strictly speaking, 1 is all we need, because the requires/ensures declared by the trait can call out to spec methods that the implementation can override:
One example that we'll encounter right away is the
Indextrait. In this case, I think it would be most flexible to have the requires/ensures call out to spec methods, as in the code shown above. Otherwise, the declared requires/ensures might just be a placeholder that's not very useful on its own, likerequires(false); ensures(true), which would then be overridden via the approaches described in 2 or 3.I'm advocating approach 1, particularly for the initial implementation. It's the simplest approach and it encourages programmers to factor their specifications into spec methods for maximum flexibility.
Beta Was this translation helpful? Give feedback.
All reactions