forked from rust-lang/rust
-
Notifications
You must be signed in to change notification settings - Fork 1
rwlock_verified.rs
Travis Hance edited this page Apr 26, 2021
·
5 revisions
#![allow(dead_code)]
// Note: this exploration file won't make much sense unless
// you understand the RWLock.dfy example first.
// Also see rwlock_unverified.rs.
// Let's assume we have some basic primitives for manipulating ghost state:
use galvanize::GhostTokenStore;
use galvanize::PCM;
use galvanize::PCMExtension;
// A replacement for UnsafeCell (equivalent, except that it's not unsafe
// because we use ghost state to verify its behavior)
use galvanize::VerifiedCell;
// A library for verified atomics using ghost state:
use galvanize::atomic::{AtomicU32, AtomicBool, Ordering};
// Define the extension PCM like in the RWLock.dfy file:
struct CentralState<Base> {
logical_exc: bool,
logical_rc: nat,
held_value: Option<Base>,
}
// Auto-derive the monoidal properties
// (Option and nat are simple 'built in' monoids that allow auto-deriving)
#[derive(PCM)]
struct RWLockProtocol<Base> {
phys_exc: Option<bool>,
phys_rc: Option<nat>,
central: Option<CentralState<Base>>,
exc_pending_handle: Option<()>,
exc_taken_handle: Option<()>,
shared_pending_handles: nat,
shared_taken_handles: fn (Base) -> nat,
}
impl<Base> RWLockProtocol<Base> {
fn PhysExcHandle(phys_exc: bool) -> RWLockProtocol<Base> {
RWLockProtocol{
phys_exc: Some(phys_exc),
phys_rc: None,
central: None,
exc_pending_handle: None,
exc_taken_handle: None,
shared_pending_handles: 0,
shared_taken_handles: (|x| -> 0),
}
}
fn PhysRcHandle(phys_rc: nat) -> RWLockProtocol<Base> {
RWLockProtocol{
phys_exc: None,
phys_rc: Some(phys_rc),
central: None,
exc_pending_handle: None,
exc_taken_handle: None,
shared_pending_handles: 0,
shared_taken_handles: (|x| -> 0),
}
}
fn CentralHandle(central: CentralState) -> RWLockProtocol<Base> {
RWLockProtocol{
phys_exc: None,
phys_rc: None,
central: Some(central),
exc_pending_handle: None,
exc_taken_handle: None,
shared_pending_handles: 0,
shared_taken_handles: (|x| -> 0),
}
}
fn ExcPendingHandle() -> RWLockProtocol<Base> {
RWLockProtocol{
phys_exc: None,
phys_rc: None,
central: None,
exc_pending_handle: Some(()),
exc_taken_handle: None,
shared_pending_handles: 0,
shared_taken_handles: (|x| -> 0),
}
}
fn ExcTakenHandle() -> RWLockProtocol<Base> {
RWLockProtocol{
phys_exc: None,
phys_rc: None,
central: None,
exc_pending_handle: None,
exc_taken_handle: Some(()),
shared_pending_handles: 0,
shared_taken_handles: (|x| -> 0),
}
}
fn SharedPendingHandle() -> RWLockProtocol<Base> {
RWLockProtocol{
phys_exc: None,
phys_rc: None,
central: None,
exc_pending_handle: None,
exc_taken_handle: None,
shared_pending_handles: 1,
shared_taken_handles: (|x| -> 0),
}
}
fn SharedTakenHandle(b: Base) -> RWLockProtocol<Base> {
RWLockProtocol{
phys_exc: None,
phys_rc: None,
central: None,
exc_pending_handle: None,
exc_taken_handle: None,
shared_pending_handles: 0,
shared_taken_handles: (|x| -> (if x == b { 1 } else { 0 })),
}
}
}
// I want to somehow create a trait that ties Base and RWLockProtocol<Base>
// together. Not sure if this is possible in Rust ???
impl PCMExtension for (Base, RWLockProtocol<Base>) {
// Proof code for the invariants, transitions from the RWLock.dfy example
// all go here.
fn invariant(&m: RWLockProtocol<Base>) -> bool { ... }
fn interp(&m: RWLockProtocol<Base>) -> Base { ... }
...
}
struct RWLock<T> {
cell: VerifiedCell<T>,
// VerifiedCellValue<T> is the type of the ghost-state that gives you
// permission to access VerifiedCell<T>.
// RWLockProtocol<VerifiedCellValue<T>>, then, is a ghost state for
// the RWLockProtocol that manages access to VerifiedCell<T>.
// Finally, GhostTokenStore<...> allows atomic access to a piece of said ghost state
ghost central_token: GhostTokenStore<RWLockProtocol<VerifiedCellValue<T>>>
// Atomics store ghost state as well
exc: AtomicBool<RWLockProtocol<VerifiedCellValue<T>>>,
rc: AtomicU32<RWLockProtocol<VerifiedCellValue<T>>>,
}
impl<T> RWLock<T> {
ghost fn central_inv(t: &RWLockProtocol) {
t.central.is_some()
}
// Invariants that tie the ghost state to the actual values stored in the Atomics
ghost fn exc_inv(b: bool, t: &RWLockProtocol) {
t == PhysExcHandle(b)
}
ghost fn shared_inv(n: nat, t: &RWLockProtocol) {
t == PhysSharedHandle(n)
}
ghost fn invariant(&self) {
// We have several tokens floating around - make sure they all refer
// to the same instance of the protocol. Also make sure they correspond
// to the VerifiedCell instance they are supposed to be managing
self.central_token.loc.base == self.cell.loc
&& self.exc.loc == self.central_token.loc
&& self.rc.loc == self.central_token.loc
// Tie the Atomics and the Invariant to the invariants defined above
&& self.central_token.inv == central_inv
&& self.exc.inv == exc_inv
&& self.rc.inv == rc_inv
}
pub fn new(t: T) -> RWLock<T> {
// The 'VerifiedCell' library would let us create a 'cell token' which represents
// permission to access the contents of the VerifiedCell. So for example,
// if we have exclusive access to the cell token, we can get a &mut borrow from
// the VerifiedCell. If we have shared access to the cell token, we can get a
// shared &borrow from the VerifiedCell.
// Keep in mind the token is "ghost".
// cell: VerifiedCell<T>
// token: Token<VerifiedCellValue<T>>
let (cell, token) = VerifiedCell::new(t);
// Instantiate an RWLockProtocol to manage access to the 'cell token'.
let ghost rwlock_protocol_token = PCMExtensionCreate(token,
PhysExcHandle(false)
· PhysRcHandle(0) // · represents the monoidal binary operator
· CentralHandle(CentralState{
logical_exc: false,
logical_rc: 0,
held_value: Some(token.value()),
}));
// Split the token into three parts
// (Can we come up with some better syntax that doesn't require so much copy-paste?)
let ghost phys_exc_token, phys_rc_token, central_token = rwlock_protocol_token.split(
PhysExcHandle(false),
PhysRcHandle(0),
CentralHandle(CentralState{
logical_exc: false,
logical_rc: 0,
held_value: Some(token.value()),
}));
// Build the object.
return RWLock{
cell: VerifiedCell::new(t),
central_token: GhostTokenStore::new(central_token, central_inv),
exc: AtomicBool::new(false, phys_exc_token, exc_inv),
rc: AtomicU32::new(0, phys_rc_token, rc_inv),
};
}
pub fn acquire_exclusive(&self, fun: fn(&mut T) -> ()) {
ghost let pending_handle;
loop {
// Here, I'm imagining some special syntax for convenient access to the ghost
// state of the atomic. The idea is this:
// When you perform the compare_exchange, we are doing some transition on the
// physical staet `old_b -> new_b` (e.g., on success, we end up doing a transition
// false -> true). Inside the 'atomic' block we can execute ghost code that manipulates
// the ghost state (and returns it at the end). We have to make sure that the invariants
// are maintained.
let res = atomic self.exc.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) acquire (exc_token, old_b, new_b) {
let exc_token';
let central_token';
atomic self.central_token.open() acquire (central_token) {
if res.is_okay() {
// Here we perform a transition that lets us get the 'pending handle'.
// Again, see the RWLock.dfy file (acquire_exc_pending_step)
exc_token', central_token', pending_handle = RWLockProtocol::transition_exc_pending(
exc_token, central_token);
} else {
// If the compare_exchange fails, then don't do anything.
exc_token' = exc_token;
central_token' = central_token;
}
release central_token';
}
release exc_token;
}
if res.is_ok() {
break;
}
}
// At this point we've obtained an ExcPendingHandle token.
// Now for the second phase of the 'acquire' step.
// (acquire_exc_finish_step)
ghost let cell_token;
loop {
let r = atomic self.rc.load(Ordering::SeqCst) acquire (rc_token) {
if rc == 0 {
atomic self.central_token.open() acquire (central_token) {
rc_token, central_token, cell_token = RWLockProtocol::transition_exc_finish(rc_token, central_token, pending_handle);
release central_token;
}
}
release rc_token;
}
if r == 0 {
break;
}
}
// This step lets us obtain the CellToken! Remember, we originally put the
// Cell token "into the RWLockProtocol world". Here, we've now "extracted"
// it from that world and we can manipulate it normally. Since we own the CellToken,
// we can get &mut access to the contents of the VerifiedCell.
let borrow = &mut *self.t.get_mut(cell_token);
fun(&mut borrow);
// borrow expires, we get cell_token back?
// this part is a little awkward
// Do the release step (i.e., put the CellToken back into the RWLockProtocol world)
let res = atomic self.exc.store(false, Ordering::SeqCst) acquire(exc_token) {
atomic self.central_token.open() acquire (central_token) {
exc_token, central_token = RWLockProtocol::release_exc(exc_token, central_token, cell_token);
release central_token;
}
release exc_token;
}
}
pub fn acquire_shared(&self, fun: fn(& T) -> ()) {
// Process for acquiring shared access is pretty similar at first.
let shared_handle;
loop {
loop {
let r = self.exc.load(Ordering::Relaxed);
if !r { break; }
}
atomic self.rc.fetch_add(1, Ordering::SeqCst) {
// ... (acquire_shared_pending_step)
}
let already_taken = atomic self.exc.load(Ordering::SeqCst) {
if !already_taken {
// ... (acquire_shared_finish)
} else {
}
}
if !already_taken {
break;
} else {
// abort and try again
atomic self.rc.fetch_sub(1, Ordering::SeqCst) {
// ...
}
}
}
// The main difference here is that we get a 'shared borrow' of the cell_token
// rather than a 'mut borrow'.
let cell_token = & RWLockProtocol::borrow(shared_handle);
// With that 'shared borrow' we can get a 'shared borrow' of the object in the
// VerifiedCell.
let borrow = self.cell.get_borrow(cell_token);
fun(borrow);
atomic self.rc.fetch_sub(1, Ordering::SeqCst) {
// ...
}
}
}