Assume-guarantee reasoning is a family of methods for modular verification of concurrent systems.
They are generally related to Jones' "rely-guarantee" reasoning of the early 80s, which in turn is an extension of an earlier Hoare-logic variant for parallel verification developed by Owicki and Gries in the late 70s.
The key concepts in all these approaches are:
- Abstracting-away portions of the system into assumptions.
- Verifying that the remaining un-abstracted portion of the system upholds guarantees.
- Checking non-interference between abstracted and verified portions of the system, to ensure soundness of the abstraction.
- Checking coverage of all assertions in all mixtures of abstraction and verification.
Ivy uses isolates as its units of assume-guarantee reasoning, along with visibility qualifiers.
Specifically, when verifying some isolate X:
- The actions and state variables of
Xare preserved and verified, as well as all actions and state variables at thespecificationvisibility level of any other isolates listed in thewithportion ofX. - The contents of all other isolates in the program -- including
implementationandprivatevisibility levels of isolates listed in thewithclause ofX-- are abstracted (erased) from verification. - Any action that is to be abstracted is first checked to ensure that it does not modify any of the same state variables as any of the actions in
X. If it does, an error is signalled. - All guarantees in the system are checked for coverage from the perspective of each isolate.
Furthermore, all preconditions and postconditions are contextually interpreted as assumptions and guarantees, depending on caller/callee relationship to the isolate X.
Specifically:
The following are guarantees in
X(that verification must prove):- A
requireassertion in another isolate thatXcalls into An
ensureassertion withinXThe following are assumptions in
X(that verification may assume):- A
requireassertion withinX - An
ensureassertion in another isolate thatXcalls into
- A
Example:
Suppose we have the following simple client/server system:
isolate client = {
specification {
type count
interpret count -> int
var n_send:count
var n_recv:count
action send_req = {
n_send := n_send + 1;
call server.req;
}
action recv_res = {
require n_send > n_recv;
n_recv := n_recv + 1;
}
invariant n_recv <= n_send
}
implementation {
after init {
n_send := 0;
n_recv := 0;
}
}
} with server
isolate server = {
specification {
action req
before req {
require client.n_send > client.n_recv;
}
}
implementation {
implement req {
call client.recv_res;
}
}
} with client
export client.send_req
export client.recv_res
export server.req
Each isolate verifies on its own:
- The client actions establish and maintain the invariant
- The client
recv_resaction has a precondition which is treated as an assumption guaranteed by the environment while verifyingclient-- and this allows it to guarantee the invariant. - The precondition on
recv_resis met by theserverrequiring it as well in itsspecification
If we begin hiding parts of the implementation, however, we run into a problem. For example if we hide the implementation of send_req and recv_res (while preserving the precondition of recv_res in a mixin):
isolate client = {
specification {
type count
interpret count -> int
var n_send:count
var n_recv:count
action send_req
action recv_res
#+++ New mixin added here
before recv_res {
require n_send > n_recv
}
invariant n_recv <= n_send
}
implementation {
after init {
n_send := 0;
n_recv := 0;
}
#+++ New implement blocks added here
implement send_req {
n_send := n_send + 1;
call server.req;
}
implement recv_res {
n_recv := n_recv + 1;
}
}
} with server
isolate server = {
specification {
action req
before req {
require client.n_send > client.n_recv;
}
}
implementation {
implement req {
call client.recv_res;
}
}
} with client
export client.send_req
export client.recv_res
export server.req
Now client still verifies, but verifying the server isolate now fails:
Isolate server:
t.ivy: line 34: error: Call out to client.recv_res[implement5] may have visible effect on client.n_recv
t.ivy: line 12: referenced here
t.ivy: line 34: referenced here
This happens because Ivy tracks which actions modify which state variables, in particular it notices that client.recv_res modifies client.n_recv.
Ivy then attempts to verify server in isolation, and its first step is to check that actions outside server can be safely abstracted (erased). Erasing client.recv_res is only safe if the variables it alters are only used by other abstract (to-be-erased) actions.
Unfortunately client.n_recv is mentioned in the (still-visible, not-erased) precondition in the mixin before client.recv_res, and that precondition is a guarantee that server has to maintain. It cannot safely do so if client.recv_res is abstracted away, as its effect on client.n_recv will be unknown.
If we attempt to recover from this by moving the precondition on client.recv_res into the implement block as well, as follows:
isolate client = {
specification {
type count
interpret count -> int
var n_send:count
var n_recv:count
action send_req
action recv_res
invariant n_recv <= n_send
}
implementation {
implement send_req {
n_send := n_send + 1;
call server.req;
}
implement recv_res {
require n_recv < n_send;
n_recv := n_recv + 1;
}
after init {
n_send := 0;
n_recv := 0;
}
}
} with server
isolate server = {
specification {
action req
}
implementation {
implement req {
call client.recv_res;
}
}
} with client
export client.send_req
export client.recv_res
export server.req
We get a different error:
t.ivy: line 19: error: assertion is not checked
t.ivy: line 10: error: ...in action client.recv_res
t.ivy: line 35: error: ...when called from server.req[implement6]
error: Some assertions are not checked
This is a coverage error: when verifying server, Ivy will abstract-away (erase) the implementation of recv_res, which means an precondition that is only in the isolate's implementation will not be checked.
The best we can do, then, is change the implementation assertion into a dynamic condition, which will at least still preserve the isolate's weaker invariant:
isolate client = {
specification {
type count
interpret count -> int
var n_send:count
var n_recv:count
action send_req
action recv_res
invariant n_recv <= n_send
}
implementation {
implement send_req {
n_send := n_send + 1;
call server.req;
}
implement recv_res {
if n_recv < n_send {
n_recv := n_recv + 1;
}
}
after init {
n_send := 0;
n_recv := 0;
}
}
} with server
isolate server = {
specification {
action req
}
implementation {
implement req {
call client.recv_res;
}
}
} with client
export client.send_req
export client.recv_res
export server.req