Returning mutable references #35
Replies: 5 comments 10 replies
-
"Re-assigning to a pointer variable" refers to cases like this: let a = 13;
let b = 15;
let mut c = &mut a;
if (cond) {
c = &mut b;
}
*c += 1; Re-borrowing is cases like this: if let Some(ref mut a) = hash_map.get_mut(key) { // the original borrow is the &mut returned by `get_mut`
// `a` re-borrows the contents of the returned `&mut`
} |
Beta Was this translation helpful? Give feedback.
-
So for the mem allocator, I'm starting to think it's going to be really helpful to have more &mut support. Especially things to get &mut references from pointers which requires functions that return &mut. @jaybosamiya also mentioned that additional &mut support would be helpful for whatever he's doing. I'd like to start moving forward on this. Here's my perspective on the design: The RustHorn encoding, which uses prophecy variables, is very clean. I think it's probably the encoding that will be easiest to implement VC generation for; it is very general (supporting containers that contain &mut types, or assigning to a reference variable, for example); it does not require much "special casing" of One downside is that it might be a little more challenging for users to grok. I think the 'pledge' style, as @utaal illustrates in a bunch of examples above, may be a little easier to understand, and could possibly lead to better diagnostics (e.g., "This borrow expires on line XY, but the pledge at line ZW from this call is not satisfied"). However, since I think the RustHorn prophecy encoding makes for the most general foundation, I think it might be better to start there, and then if it's hard to use, add pledge-style specifications as a special syntax to aid diagnostics. The other challenge for the prophecy encoding is that we need to implement prophecy variables soundly, although there are some other things I think prophecy variables might be useful for, so I don't see this as much of a downside. The Prophecy EncodingThe prophecy encoding represents a For example, suppose you have a variable
Here's a worked example: AIR-level encoding concrete values
let mut x = 5; assign x := 5 x: 5
let mut x_ref = &mut x; assign x_ref := (x, prophecy_x)
assign x := prophecy_x x: 20, x_ref: (5, 20)
*x_ref = 7; assign x_ref := (7, x_ref.1) x: 20, x_ref: (7, 20)
*x_ref = 20; assign x_ref := (20, x_ref.1) x: 20, x_ref: (20, 20)
/* x_ref expires */ assume (x_ref.0 == x_ref.1)
assert(x == 20); assert (x == 20); The assert at the end follows because:
Method calls with
|
Beta Was this translation helpful? Give feedback.
-
Proposal for resolving the current challenges with
|
Beta Was this translation helpful? Give feedback.
-
I read this discussion and think parts of my MSc thesis https://dx.doi.org/10.14288/1.0438326 are relevant and might be helpful. Chapter 2 is about designs for a specification language which is largely expression-based and tries to "follow Rust rules", based on the syntax used in Prusti but with a lot more expressiveness than the tool supports (particularly w.r.t. mutable references). It ultimately presents a type system that classifies whether a specification is well-formed or not (essentially: whether expressions are being evaluated in a state that Rust would allow). This chapter also includes a translation of these well-typed specifications to Creusot's prophecy-based specification language. This would potentially enable a tool to allow users to interact with a Prusti-like (expression-based) syntax while giving a prophecy-based encoding to the SMT solver. Chapter 3 deals with the soundness of combining the prophecy encoding used in Creusot with ghost state and shows that this can be made sound when limiting the operations allowed when creating ghost types. Following this proposal, Creusot ended up splitting its |
Beta Was this translation helpful? Give feedback.
-
@tjhance you gave the following example for how ensure clauses could be use to specify contracts involving struct Animal {
cat: u64,
dog: u64,
}
fn get_cat_ref(animal: &mut Animal) -> (cat: &mut u64)
ensures
new(animal).dog == old(animal).dog, // animal.dog isn't changed
old(cat) == old(animal).cat, // the value of `*cat` upon return
^ Note: should't this be 'cat' instead of 'old cat', since the variable `cat` did not exist before the call?
new(cat) == new(animal).cat, // tells how the value of `*cat` flows back to `animal.cat` upon expiration From a user perspective, that looks cumbersome to me, because I need to specify everything that's not changed by Also, shouldn't the ensure clause also specify the value of animal.cat == old(animal.cat),
animal.dog == old(animal.dog), My gut feeling is it would be good if by default the contract assumes that values in the contexts beforeCall(old), afterCall and afterBorrow(new) are equal to each other, unless otherwise specified. I can imagine you would prefer to save discussion what's an optimal syntax to later, but for the sake of providing predictable verification time, I think there's value in having the size of the SMT encoding match the size of the specification provided by the user, so then the choice for syntax could influence the encoding as well. For example, something which I question about the current limited support for struct S {
a: u64,
...
z: i64
}
fn updateA(s: &mut S)
{
s.a = 1;
} Generates AIR code for |
Beta Was this translation helpful? Give feedback.
-
I'm looking into how to support functions that return mutable references. These are generally very useful for things like mutating (part of) an element of a vector, without having to move it out (which we had to do with Linear Dafny).
There are multiple parts to this feature:
I've written up my current ideas for the specification here:
Support for returning mutable references
This concerns functions that return mutable references (e.g.
fn index_mut(&mut self, idx: usize) -> &mut u64
); it does not yet consider support for borrows without a function call (e.g.let a = &mut adt.field
).Function spec, from the perspective of the callers
Similarly to calling functions with an
&mut
parameter, we can treat (a) returning a mutable reference and (b) the borrow expiring as two "moves" of the borrowed value: once (a) out of and once (b) back to the lender (the lender is the&mut
parameter with the same lifetime as the returned reference). This is justified because with mutable borrows an exclusive permission to the borrowed value is transferred for the duration of the borrow.The intuition is to treat the caller's code from the call site to the end of the borrow roughly as if it was a function that takes the mutable reference as an
&mut
parameter. The specification of this ficticious function is provided in anafter<'lifetime>
block in the header of the borrowing function. In other words,requires
andensures
apply to the function call site, and have the same semantics as for functions that don't return a mutable reference;after<'lifetime>
block is used to introduce a second pair ofrequires
/ensures
that refer to the point at which the borrow expires:requires
expresses constraints on the borrowed value before the borrow ends (i.e. before it's returned to its lender),ensures
is what's true after the end of the borrow.before(ref)
whereref
is the reference can be used to refer to the value of the borrowed references just after the call (at the very start of the borrow), whereasold
has the same meaning as regularrequires
/ensures
In its unsugared form,
after
requires an explicit lifetime, to tie it to a specific lifetime being returned by the function. Despite being initially unsupported, a rust function may return multiple mutable references with different lifetimes. We can provide a version ofafter
with an implicit lifetime, when only one lifetime is (implicitly or explicitly) declared for the function signature.Caller code
In the caller code, the mutably borrowed value (the lender) is treated as "moved" for the duration of the borrow (the borrow-checker would prevent access to it): referencing its value in spec code results in the value before the move (i.e. the call). It is havoc-ed when the borrow ends.
One language design issue is the fact that generally speaking there's no code that directly corresponds to the end of a borrow, as shown in Example 3 and Example 4. To start (and in order to avoid having to extract information from the borrow checker), we can require the user to explicitly expire the borrow (with an
expire
/drop
function), like in Example 4.Example 1. Correct usage:
Example 2. Error,
after
requires
not satisfied:Example 3. Error,
after
ensures
only takes hold at the end of the borrow:Example 4. Example 3 fixed: explicitly drop the borrowing variable to terminate the borrow:
This discussed the general case of a borrow extending for potentially multiple statements. The special case of a single direct assignment to the mutable reference in the same statement as the function call can be handled using the same semantics as described above (but without the need for an explicit
expire
).Example 5.
Example 6.
Option<&mut>
brought up by @tjhance. A function on a hashtable to optionally obtain a mutable reference to a value, if the key is present:This example depends on implementing
enum
.is_variant()
and.field()
, similar to Dafny's.Variant?
(e.g..Some?
) and Dafny's field accessors (forenum
s).Example 6b. Caller, without reborrows, but with helper functions on
Option
:Example 6c. Caller, with reborrows
val
bound in the match pattern:Prusti1's pledges
The
ensures
clause inafter
is similar to Prusti's pledges, as it addresses the same issue, how to specify what happens due to the modification of the part of the lender that was returned by the function as a mutable borrow. Therequires
clause inafter
seems however to not have a direct equivalent in Prusti's pledges (it could however be encoded as an implication that would result in a weak postcondition if the antecedent encoding the required final value of the borrow is false).Example. From the Prusti paper, adapted to Verus' syntax.
Verification conditions for the function body
Given a
we can write a function
Open questions
I believe that what I'm proposing is not too dissimilar from Prusti's "pledges" (but the encoding may turn out different).
I will be extending the doc with a bit more about my plans for the VC generation (initially, for the call-site).
The doc also has a stub regarding VC to check the function body, but I'm currently leaving that for later: this is because we have multiple immediate uses of functions returning
&mut
where the body is trusted (e.g. Vec'sindex_mut
). It is important to keep in mind to avoid deciding on a specification language that ends up awkward for body VCs.I would appreciate input on what you think about the usability of the function's specification constructs (
after
), and what you think of the semantics at the call site (borrow expiration, potentially required to be explicit initially)I’m planning this in part to be an incremental path, where initially we, for example, do not support re-assigning to a pointer variable (a challenge that the RustHorn paper tries to address, I believe) and we do not support re-borrowing (i.e. constructing a mutable reference to a part of a value pointed by an existing mutable reference).
Footnotes
Leveraging Rust Types for Modular Specification and Verification -- http://pm.inf.ethz.ch/publications/getpdf.php?bibname=Own&id=AstrauskasMuellerPoliSummers19b.pdf ↩
Beta Was this translation helpful? Give feedback.
All reactions