Bisimulation Prover
SAW contains a bisimulation prover to prove that two terms simulate each other.
This prover allows users to prove that two terms executing in lockstep satisfy
some relations over the state of each circuit and their outputs. This type of
proof is useful in demonstrating the eventual equivalence of two circuits, or
of a circuit and a functional specification. SAW enables these proofs with the
experimental prove_bisim
command:
prove_bisim : ProofScript () -> [BisimTheorem] -> Term -> Term -> Term -> Term -> TopLevel BisimTheorem
When invoking prove_bisim strat theorems srel orel lhs rhs
, the arguments
represent the following:
strat
: A proof strategy to use during verification.theorems
: A list of already proven bisimulation theorems.srel
: A state relation betweenlhs
andrhs
. This relation must have the typelhsState -> rhsState -> Bit
. The relation’s first argument islhs
’s state prior to execution. The relation’s second argument isrhs
’s state prior to execution.srel
then returns aBit
indicating whether the two arguments satisfy the bisimulation’s state relation.orel
: An output relation betweenlhs
andrhs
. This relation must have the type(lhsState, output) -> (rhsState, output) -> Bit
. The relation’s first argument is a pair consisting oflhs
’s state and output following execution. The relation’s second argument is a pair consisting ofrhs
’s state and output following execution.orel
then returns aBit
indicating whether the two arguments satisfy the bisimulation’s output relation.lhs
: A term that simulatesrhs
.lhs
must have the type(lhsState, input) -> (lhsState, output)
. The first argument tolhs
is a tuple containing the internal state oflhs
, as well as the input tolhs
.lhs
returns a tuple containing its internal state after execution, as well as its output.rhs
: A term that simulateslhs
.rhs
must have the type(rhsState, input) -> (rhsState, output)
. The first argument torhs
is a tuple containing the internal state ofrhs
, as well as the input torhs
.rhs
returns a tuple containing its internal state after execution, as well as its output.
On success, prove_bisim
returns a BisimTheorem
that can be used in future
bisimulation proofs to enable compositional bisimulation proofs. On failure,
prove_bisim
will abort.
Bisimulation Example
This section walks through an example proving that the Cryptol implementation of an AND gate that makes use of internal state and takes two cycles to complete is equivalent to a pure function that computes the logical AND of its inputs in one cycle. First, we define the implementation’s state type:
type andState = { loaded : Bit, origX : Bit, origY : Bit }
andState
is a record type with three fields:
loaded
: ABit
indicating whether the input to the AND gate has been loaded into the state record.origX
: ABit
storing the first input to the AND gate.origY
: ABit
storing the second input to the AND gate.
Now, we define the AND gate’s implementation:
andImp : (andState, (Bit, Bit)) -> (andState, (Bit, Bit))
andImp (s, (x, y)) =
if s.loaded /\ x == s.origX /\ y == s.origY
then (s, (True, s.origX && s.origY))
else ({ loaded = True, origX = x, origY = y }, (False, 0))
andImp
takes a tuple as input where the first field is an andState
holding
the gate’s internal state, and second field is a tuple containing the inputs to
the AND gate. andImp
returns a tuple consisting of the updated andState
and
the gate’s output. The output is a tuple where the first field is a ready bit
that is 1
when the second field is ready to be read, and the second field
is the result of gate’s computation.
andImp
takes two cycles to complete:
The first cycle loads the inputs into its state’s
origX
andorigY
fields and setsloaded
toTrue
. It sets both of its output bits to0
.The second cycle uses the stored input values to compute the logical AND. It sets its ready bit to
1
and its second output to the logical AND result.
So long as the inputs remain fixed after the second cycle, andImp
’s output
remains unchanged. If the inputs change, then andImp
restarts the
computation (even if the inputs change between the first and second cycles).
Next, we define the pure function we’d like to prove andImp
bisimilar to:
andSpec : ((), (Bit, Bit)) -> ((), (Bit, Bit))
andSpec (_, (x, y)) = ((), (True, x && y))
andSpec
takes a tuple as input where the first field is ()
, indicating that
andSpec
is a pure function without internal state, and the second field is a
tuple containing the inputs to the AND function. andSpec
returns a tuple
consisting of ()
(again, because andSpec
is stateless) and the function’s
output. Like andImp
, the output is a tuple where the first field is a ready
bit that is 1
when the second field is ready to be read, and the second field
is the result of the function’s computation.
andSpec
completes in a single cycle, and as such its ready bit is always 1
.
It computes the logical AND directly on the function’s inputs using Cryptol’s
(&&)
operator.
Next, we define a state relation over andImp
and andSpec
:
andStateRel : andState -> () -> Bit
andStateRel _ () = True
andStateRel
takes two arguments:
An
andState
forandImp
.An empty state (
()
) forandSpec
.
andStateRel
returns a Bit
indicating whether the relation is satisified. In
this case, andStateRel
always returns True
because andSpec
is stateless
and therefore the state relation permits andImp
to accept any state.
Lastly, we define a relation over andImp
and andSpec
:
andOutputRel : (andState, (Bit, Bit)) -> ((), (Bit, Bit)) -> Bit
andOutputRel (s, (impReady, impO)) ((), (_, specO)) =
if impReady then impO == specO else True
andOutputRel
takes two arguments:
A return value from
andImp
. Specifically, a pair consisting of anandState
and a pair containing a ready bit and result of the logical AND.A return value from
andSpec
. Specifically, a pair consisting of an empty state()
and a pair containing a ready bit and result of the logical AND.
andOutputRel
returns a Bit
indicating whether the relation is satisfied. It
considers the relation satisfied in two ways:
If
andImp
’s ready bit is set, the relation is satisfied if the output valuesimpO
andspecO
fromandImp
andandSpec
respectively are equivalent.If
andImp
’s ready bit is not set, the relation is satisfied.
Put another way, the relation is satisfied if the end result of andImp
and
andSpec
are equivalent. The relation permits intermediate outputs to differ.
We can verify that this relation is always satisfied–and therefore the two
terms are bisimilar–by using prove_bisim
:
import "And.cry";
enable_experimental;
and_bisim <- prove_bisim z3 [] {{ andStateRel }} {{ andOutputRel }} {{ andImp }} {{ andSpec }};
Upon running this script, SAW prints:
Successfully proved bisimulation between andImp and andSpec
Building a NAND gate
We can make the example more interesting by reusing components to build a NAND
gate. We first define a state type for the NAND gate implementation that
contains andImp
’s state. This NAND gate will not need any additional state,
so we will define a type nandState
that is equal to andState
:
type nandState = andState
Now, we define an implementation nandImp
that calls andImp
and negates the
result:
nandImp : (nandState, (Bit, Bit)) -> (nandState, (Bit, Bit))
nandImp x = (s, (andReady, ~andRes))
where
(s, (andReady, andRes)) = andImp x
Note that nandImp
is careful to preserve the ready status of andImp
.
Because nandImp
relies on andImp
, it also takes two cycles to compute the
logical NAND of its inputs.
Next, we define a specification nandSpec
in terms of andSpec
:
nandSpec : ((), (Bit, Bit)) -> ((), (Bit, Bit))
nandSpec (_, (x, y)) = ((), (True, ~ (andSpec ((), (x, y))).1.1))
As with andSpec
, nandSpec
is pure and computes its result in a single
cycle.
Next, we define a state relation over nandImp
and nandSpec
:
nandStateRel : andState -> () -> Bit
nandStateRel _ () = True
As with andStateRel
, this state relation is always True
because nandSpec
is stateless.
Lastly, we define an output relation indicating that nandImp
and nandSpec
produce equivalent results once nandImp
’s ready bit is 1
:
nandOutputRel : (nandState, (Bit, Bit)) -> ((), (Bit, Bit)) -> Bit
nandOutputRel (s, (impReady, impO)) ((), (_, specO)) =
if impReady then impO == specO else True
To prove that nandImp
and nandSpec
are bisimilar, we again use
prove_bisim
. This time however, we can reuse the bisimulation proof for the
AND gate by including it in the theorems
paramter for prove_bisim
:
prove_bisim z3 [and_bisim] {{ nandStateRel }} {{ nandOutputRel }} {{ nandImp }} {{ nandSpec }};
Upon running this script, SAW prints:
Successfully proved bisimulation between nandImp and nandSpec
Understanding the proof goals
While not necessary for simple proofs, more advanced proofs may require
inspecting proof goals. prove_bisim
generates and attempts to solve the
following proof goals:
OUTPUT RELATION THEOREM:
forall s1 s2 in.
srel s1 s2 -> orel (lhs (s1, in)) (rhs (s2, in))
STATE RELATION THEOREM:
forall s1 s2 out1 out2.
orel (s1, out1) (s2, out2) -> srel s1 s2
where the variables in the forall
s are:
s1
: Initial state forlhs
s2
: Initial state forrhs
in
: Input value tolhs
andrhs
out1
: Initial output value forlhs
out2
: Initial output value forrhs
The STATE RELATION THEOREM
verifies that the output relation properly captures
the guarantees of the state relation. The OUTPUT RELATION THEOREM
verifies
that if lhs
and rhs
are executed with related states, then the result of
that execution is also related. These two theorems together guarantee that the
terms simulate each other.
When using composition, prove_bisim
also generates and attempts to solve
the proof goal below for any successfully applied BisimTheorem
in the
theorems
list:
COMPOSITION SIDE CONDITION:
forall g_lhs_s g_rhs_s.
g_srel g_lhs_s g_rhs_s -> f_srel f_lhs_s f_rhs_s
where
f_lhs_s = extract_inner_state g_lhs g_lhs_s f_lhs
f_rhs_s = extract_inner_state g_rhs g_rhs_s f_rhs
where g_lhs
is an outer term containing a call to an inner term f_lhs
represented by a BisimTheorem
and g_rhs
is an outer term containing a call
to an inner term f_rhs
represented by the same BisimTheorem
. The variables
in COMPOSITION SIDE CONDITION
are:
extract_inner_state x x_s y
: A helper function that takes an outer termx
, an outer statex_s
, and an inner termy
, and returns the inner state ofx_s
thatx
passes toy
.g_lhs_s
: The state forg_lhs
g_rhs_s
: The state forg_rhs
g_srel
: The state relation forg_lhs
andg_rhs
f_srel
: The state relation forf_lhs
andf_rhs
f_lhs_s
: The state forf_lhs
, as represented ing_lhs_s
(extracted usingextract_inner_state
).f_rhs_s
: The state forf_rhs
, as represented ing_rhs_s
(extracted usingextract_inner_state
).
The COMPOSITION SIDE CONDITION
exists to verify that the terms in the
bisimulation relation properly set up valid states for subterms they contain.
Limitations
For now, the prove_bisim
command has a couple limitations:
lhs
andrhs
must be named functions. This is becauseprove_bisim
uses these names to perform substitution when making use of compositionality.Each subterm present in the list of bisimulation theorems already proven may be invoked at most once in
lhs
orrhs
. That is, if some functiong_lhs
callsf_lhs
, andprove_bisim
is invoked with aBisimTheorem
proving thatf_lhs
is bisimilar tof_rhs
, theng_lhs
may callf_lhs
at most once.