Next-generation of Tracked/Ghost code #1301
Replies: 6 comments 12 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 |
|
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 |
|
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 |
|
BTW, attribute macros follow the new style to use tracked/ghost code, but the way to pass the ghost variable could be benefited by a real functin-based ghost argument passing. |
|
We've been using |
|
Recording and synthesizing a few ideas from the Verus Retreat:
Mental model to reason about the feature when it comes to call sites. This is independent of the syntax used to define a function involving tracked/ghost values. While it makes sense to separate tracked/ghost values in a function's signature, the benefit of doing so at a call site seems small compared to the added complexity. Notionally, a function Example function and its notional signatures: // defined in the current syntax
fn f(x: f32, y: f32) -> f32
with (tracked tr: bool, ghost gh: u8),
output (ghost i32, f32),
{ ... }
// tracked `f` signature
fn tracked_f(x: f32, y: f32, tr: Tracked<bool>, gh: Ghost<u8>) -> (f32, Ghost<i32>, Tracked<f32>);
// erased `f` signature
fn f(x: f32, y: f32) -> f32;Primary goal: explicitly mark each call site syntactically. For example, the let x: f32 = ...;
let y: f32 = ...;
let tracked tr = ...;
let ghost gh = ...;
// call `f` as erased (in non-verified code)
let z = f(x, y);
// call `f` as tracked (in verified code)
let (z, ghost i, tracked w) = tracked f(x, y, tr, gh);Secondary goal: provide syntactic sugar to automate plumbing. For example, when a tracked function has only one input, support automatically "unpacking" the tracked/ghost arguments from a tuple, e.g. the result of another call. Given another function // tracked `g` with tracked/ghost values present in the signature
fn tracked_g(z: f32, i: Ghost<i32>, w: Tracked<f32>) -> bool;
// erased `g` with tracked/ghost values absent from the signature
fn g(z: f32) -> bool;That syntactic sugar would allow the following shorthand form: // sugary syntax
let flag = tracked g(tracked f(x, y, tr, gh));
// desugars into
let flag = {
let (z, ghost i, tracked w) = tracked f(x, y, tr, gh);
tracked g(z, i, w)
};Syntax
|
Uh oh!
There was an error while loading. Please reload this page.
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 extratrackedargument) instead of just writing*x = 5which 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
permargument, 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
withclause now entirely looks like part of the spec. It should be possible to attachextra_trackedsignatures toexternal_fn_specificationspecifications 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
requiresclause shows that we can always inferold_valfrompoints_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:
All reactions