Next-generation of Tracked/Ghost code #1301
Replies: 3 comments 11 replies
-
But I am wondering whether it would be possible to make it closer to Rust style. For example, user can add proc-macro attributes on the functions that needs tracked variables.
and pass the tracked variable similarly
and |
Beta Was this translation helpful? Give feedback.
-
Overall, I really like the idea proposed at the top-level. Regarding syntax, just thinking ahead for what might work well for parsers and such (ignore actual keywords, just focusing on structure), we probably want to think about binding strength, and attempting to keep things more prefix-focused, otherwise, it becomes hard to identify what this intends to mean: *ptr = foo(a, b) with(&mut x); If we instead make things prefixed, then there is an obvious difference between these two, and it is immediately clear which one the with(&mut x) *ptr = foo(a, b);
// AND
*ptr = with(&mut x) foo(a, b); Essentially, by placing it in the prefix, it becomes tightly bound to the immediate next call or syntactic structure, rather than requiring large amount of look-ahead. Once we move it to the prefix position, it almost looks like it is almost-trivial syntax sugar over attributes, which should hopefully keep implementation simple too. Note: I also suggest playing return values within the |
Beta Was this translation helpful? Give feedback.
-
I agree -- this seems like it could be a very nice improvement! I think dealing with return values seems like one of the trickier syntactic aspects. @jaybosamiya-ms: Using |
Beta Was this translation helpful? Give feedback.
-
Over time, a number of challenges with the current Tracked/Ghost system have become apparent.
I believe it is time to start sketching out the 'next generation', with an even greater separation of spec/proof & exec code.
Challenges with the current system
Embedding Tracked/Ghost in exec code is confusing
New users are always confused about how to use Tracked/Ghost and when they need to do it versus using tracked/ghost. This is understandable as the rules are pretty confusing, even if they follow logically from some implementation-constrained premises. The rules are documented here, and I frequently point users to this page, but I always feel a little embarrassed when I do so.
The need to put Tracked/Ghost in signatures and structs makes it difficult to verify 'existing' code
There are a number of problems here. For one, it makes Verus code look less familiar, e.g., you have to call
vstd::raw_ptr::ptr_mut_write
(which takes an extratracked
argument) instead of just writing*x = 5
which again makes it hard to explain what's going on. When you look at a verified function with a lot of ghost code, it can be hard to visually separate out what's what.This is also a huge obstacle for verifying 'existing' code. Signatures and structs have to be modified internally, and if you want to apply a specification to an existing external function that requires extra ghost state, you have to create a wrapper function. Besides being inconvenient, @jonhnet has pointed out that implementing the wrapper function is confusing because the ghost operation is axiomatic, but you still have to construct the ghost state in the external_body function, which is a meaningless operation but is needed to satisfy the type-checker.
You also have to add stuff to structs, e.g., a doubly-linked list looks like:
Specs about Tracked code are ugly
Here's
vstd::raw_ptr::ptr_mut_write
:Ugh! First you have to supply an extra
perm
argument, and then you have to write additional spec code about it. By contrast, in separation logic, the spec would look like:∀ ptr, value, value’. { ptr ↦ value }
*ptr = value’
{ ptr ↦ value’ }This looks nice and concise in part because it's written in terms of these universally quantified variables. This usually works out pretty well in something like Coq/Iris where the variables are usually inferred by coq's unification engine. In Verus, this kind of style is a lot harder to pull off. You can sort of systematically approximate it by adding extra Ghost params, but this sucks because (i) they look like Tracked variables even though their functional role is completely different and (ii) they then have to be supplied as arguments at the call-sites, i.e., it just exacerbates the previous 2 problems.
New approach sketch
Function signatures
First, tracked/ghost state should look entirely separate. I have no idea what the keywords / notation should be. But I imagine something isomorphic to:
At the source level, this
with
clause now entirely looks like part of the spec. It should be possible to attachextra_tracked
signatures toexternal_fn_specification
specifications as well. To call this, the tracked arguments are visually separate:With special cases for primitive Rust syntax:
We can also separate out return values:
And some kind of notation to deal with those at the call-site.
Tracked vs Ghost
We can have 2 different clauses types, one for ghost and one for Tracked params. Again, names TBD, but what I have in mind is that this:
Inference
Separating these params from the signature opens the door to do more inference, since these parameters won't be required by the surface-level type checker. In some circumstances, Verus may be able to fill them in. Admittedly, inferring tracked objects may be a Very Difficult (but rewarding) Problem. Inferring Ghost params should be a bit easier:
Inspection of the
requires
clause shows that we can always inferold_val
frompoints_to
, so the caller wouldn't need to supply it.Structs
Do something similar with structs. The doubly-linked list would look like:
This is equivalent to:
And using the struct looks something like:
Beta Was this translation helpful? Give feedback.
All reactions