|
| 1 | +# Logical Atomicity in the HOCAP style |
| 2 | + |
| 3 | +In Verus, there is another way to prove linearization, based on the [HOCAP](https://kasv.dk/articles/hocap.pdf). |
| 4 | +HOCAP is a proof pattern, rather than a syntactic language feature. This makes it harder |
| 5 | +(but still possible) to [open invariants around logical atomic code](https://research.ralfj.de/iris/talk-iris2019.pdf). |
| 6 | + |
| 7 | +To use this, one needs to use the [`logatom` traits](https://verus-lang.github.io/verus/verusdoc/vstd/logatom/index.html). |
| 8 | +The main concept is that there are `Linearizer`s and `Operations` (both mutable and read-only versions). |
| 9 | +A `Linearizer` is a ghost callback that has pre- and post- conditions. |
| 10 | +It must be called to linearize an `Operation` at a valid [linearization point](https://dl.acm.org/doi/pdf/10.1145/78969.78972), |
| 11 | +where it is consumed and a `Completion` is obtained. |
| 12 | + |
| 13 | +In our examples, we will focus on [`MutLinearizer`](https://verus-lang.github.io/verus/verusdoc/vstd/logatom/trait.MutLinearizer.html), |
| 14 | +but the [`ReadLinearizer`](https://verus-lang.github.io/verus/verusdoc/vstd/logatom/trait.ReadLinearizer.html) is very similar. |
| 15 | + |
| 16 | +## How the logically atomicity proof works |
| 17 | + |
| 18 | +When we have a linearizer , we need to call `MutLinearizer::apply` at a valid linearization point. |
| 19 | +When `apply` happens, we present a [`Resource`](https://docs.rs/vstd/latest/vstd/resource/algebra/struct.Resource.html). |
| 20 | +This resource captures the concurrent object being talked about. |
| 21 | +The linearization proof comes from the fact that we can encode the pre and post conditions of the operations |
| 22 | +(which can be thought about as the logically atomic triple) |
| 23 | +to express that the ordering that the object is going through (expressed via the resource) makes sense. |
| 24 | + |
| 25 | +## Calling `apply` |
| 26 | + |
| 27 | +Consider the example of a monotonic counter (as seen in the examples dir). |
| 28 | +The ghost state is modelled after a [`GhostVarAuth`](https://verus-lang.github.io/verus/verusdoc/vstd/resource/ghost_var/struct.GhostVarAuth.html) held in an [`AtomicInvariant`](https://verus-lang.github.io/verus/verusdoc/vstd/invariant/struct.AtomicInvariant.html), |
| 29 | +and a [`GhostVar`](https://verus-lang.github.io/verus/verusdoc/vstd/resource/ghost_var/struct.GhostVar.html) which the `MutLinearizer` holds. |
| 30 | + |
| 31 | +```rust |
| 32 | +open_atomic_invariant!(inv => v => { |
| 33 | + let tracked CounterInvariant { perm, auth } = v; // destructure |
| 34 | + |
| 35 | + // curr is the loaded value |
| 36 | + res = self.atomic.compare_exchange_weak(Tracked(&mut perm), curr, next); |
| 37 | + |
| 38 | + proof { |
| 39 | + if res is Ok { |
| 40 | + let op = IncOp { id: self.auth_id() }; |
| 41 | + compl = Some(lin.apply(op, &mut auth, (), &curr)); |
| 42 | + |
| 43 | + v = CounterInvariant { perm, auth }; |
| 44 | + } else { |
| 45 | + v = CounterInvariant { perm, auth }; |
| 46 | + } |
| 47 | + } |
| 48 | +}); |
| 49 | +``` |
| 50 | +In the CAS loop to increment the counter, we open the invariant, call `compare_exchange_weak` and, |
| 51 | +on success, use the `GhostVarAuth` to update the ghost state within `apply`. |
| 52 | + |
| 53 | +## Constructing the Operations |
| 54 | + |
| 55 | +Logical atomicity and linearizability deal in operations being linearized. |
| 56 | +The `ReadOperation` and `MutOperation` model these operations that are linearized. |
| 57 | + |
| 58 | +In the counter example, we can specify an increment operation, `IncOp`: |
| 59 | + |
| 60 | +```rust |
| 61 | +/// Operation for incrementing the counter |
| 62 | +struct IncOp { |
| 63 | + /// Location of the GhostVarAuth |
| 64 | + id: Loc, |
| 65 | +} |
| 66 | + |
| 67 | +impl logatom::MutOperation for IncOp { |
| 68 | + type Resource = GhostVarAuth<u64>; |
| 69 | + |
| 70 | + type ExecResult = u64; |
| 71 | + |
| 72 | + type NewState = (); |
| 73 | + |
| 74 | + closed spec fn requires( |
| 75 | + self, |
| 76 | + pre: Self::Resource, |
| 77 | + new_state: Self::NewState, |
| 78 | + e: Self::ExecResult, |
| 79 | + ) -> bool { |
| 80 | + &&& pre.id() == self.id |
| 81 | + &&& pre@ == e |
| 82 | + } |
| 83 | + |
| 84 | + closed spec fn ensures( |
| 85 | + self, |
| 86 | + pre: Self::Resource, |
| 87 | + post: Self::Resource, |
| 88 | + new_state: Self::NewState, |
| 89 | + ) -> bool { |
| 90 | + &&& post.id() == self.id |
| 91 | + &&& post@ == pre@.wrapping_add(1) |
| 92 | + } |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +Here, we force the resource being passed in to refer to the same ghost location |
| 97 | +as the counter's resource (by having the location in the operation itself) and |
| 98 | +models the logically atomic pre and post conditions. |
| 99 | + |
| 100 | +## Constructing a Linearizer |
| 101 | + |
| 102 | +The linearizer tipically holds the resource itself. The apply function can either |
| 103 | +express agreement (for read only operations) or update the resource (for mutable operations). |
| 104 | + |
| 105 | +We can express additional pre and post conditions on the linearizer. Centrally, in the `apply` |
| 106 | +function we must be able to show that from both the operation and the linearizer preconditions |
| 107 | +we can derive both post conditions: |
| 108 | + |
| 109 | +```rust |
| 110 | +struct IncPerm { |
| 111 | + pub tracked var: GhostVar<u64>, |
| 112 | +} |
| 113 | + |
| 114 | +impl logatom::MutLinearizer<IncOp> for IncPerm { |
| 115 | + type Completion = GhostVar<u64>; |
| 116 | + |
| 117 | + closed spec fn namespaces(self) -> ISet<int> { ISet::empty() } |
| 118 | + |
| 119 | + closed spec fn pre(self, op: IncOp) -> bool { |
| 120 | + op.id == self.var.id() |
| 121 | + } |
| 122 | + |
| 123 | + closed spec fn post(self, op: IncOp, exec_res: u64, completion: Self::Completion) -> bool { |
| 124 | + &&& op.id == self.var.id() |
| 125 | + &&& op.id == completion.id() |
| 126 | + &&& exec_res.wrapping_add(1) == completion@ |
| 127 | + } |
| 128 | + |
| 129 | + proof fn apply( |
| 130 | + tracked self, |
| 131 | + op: IncOp, |
| 132 | + tracked resource: &mut GhostVarAuth<u64>, |
| 133 | + new_state: (), |
| 134 | + exec_res: &u64, |
| 135 | + ) -> (tracked result: Self::Completion) { |
| 136 | + let tracked mut var = self.var; |
| 137 | + resource.update(&mut var, exec_res.wrapping_add(1)); |
| 138 | + var |
| 139 | + } |
| 140 | + |
| 141 | + proof fn peek(tracked &self, op: IncOp, tracked resource: &GhostVarAuth<u64>) {} |
| 142 | +} |
| 143 | + |
| 144 | +``` |
0 commit comments