The logic we use is a variant of pRHL, a probabilistic relational Hoare logic introduced by Barthe et al. [
26]. The logic exposes relational judgments of the form
for which a basic intuition is provided in Section
2.2. Formally,
\(c_0\) and
\(c_1\) denote probabilistic stateful code with return type
\(A_0\) and
\(A_1\) , respectively, and the precondition
\(m_0 : \mathsf {mem}, m_1 : \mathsf {mem}\vdash \phi : \mathbb {P}\) is a proposition with free variables
\(m_0\) and
\(m_1\) denoting the initial state of the memory (before execution of the code). The postcondition
\(m_0^{\prime } : \mathsf {mem}, a_0 : A_0, m_1^{\prime } : \mathsf {mem}, a_1 : A_1 \vdash \psi : \mathbb {P}\) is a predicate on the values returned by the executed code, which is parameterized by the variables
\(m_0^{\prime }\) and
\(m_1^{\prime }\) representing the final state of the memory (after execution) and by the final values
\(a_0\) and
\(a_1\) . As mentioned before, we will sometimes omit the quantifications when they are clear from the context. We will also abuse notation and sometimes write, e.g.,
\(\psi (m_0^{\prime },a_0) (m_1^{\prime },a_1)\) for the substitution of
\(\psi\) with the given memories and values. The code fragments appearing in a judgment are drawn from the free monad
\(\texttt {code}\) \(_{\mathcal {L} , I}\) of Section
3.1, and meet the further requirement that no oracle calls
\(\texttt {call o x k}\) appear in them (exactly as in Section
3.2). The precondition
\(\phi\) is defined to be a relation between initial memories (for instance,
\(m_0 = m_1\) ). Similarly the postcondition
\(\psi\) relates final memories and final results, intuitively obtained after the execution of
\(c_i\) on
\(m_i\) . We describe how to assign a formal semantics for such probabilistic judgments in Section
5.2. The semantics is based on the notion of
probabilistic couplings, already adopted by Barthe et al. [
22]. In the remainder of this subsection, we describe a selection of our rules. The presentation does not contain all the rules employed in practice by SSProve, nor does it provide a canonical presentation of these rules: some rules are overlapping hence there are multiple ways to prove the same relational judgment, but the actual derivation might be simpler with this redundancy. We return to the question of the organization of rules after this presentation:
The
\(\texttt {reflexivity}\) rule relates the code
c to itself when both copies are executed on identical initial memories.
The
\(\texttt {seq}\) rule relates two sequentially composed commands using
\(\texttt {bind}\) by relating each of the sub-commands.
The
\(\texttt {swap}\) rule states that if a certain relation on memories
I is invariant with respect to the execution of
\(c_0\) and
\(c_1\) , then the order in which the commands are executed is not relevant. We used the
\(\texttt {swap}\) rule in Section
2.3 to swap two independent samplings; in that case the invariant
I consisted in the equality of memories.
The
eqDistrL rule allows us to replace
\(c_0\) by
\(c_0^{\prime }\) when both codes have the same denotational semantics as defined by
\(\mathtt {Pr_code}\) , in the sense of Section
3.2.
The
symmetry rule simply states that the symmetric judgment holds if the arguments of the pre- and postconditions are swapped accordingly.
The
for-loop rule relates two executions of for-loops with the same number of iterations by maintaining a relational invariant through each step of the iteration.
The
do-while rule relates two bounded while loops with bodies
\(c_0\) and
\(c_1\) . Every iteration preserves a relational invariant on memories
I that depends on a pair of Booleans, and the postcondition also stipulates that
\(c_0\) and
\(c_1\) return the same Boolean, i.e.,
\(b_0 = b_1\) . This rule follows the pattern of the unbounded do-while rule defined for simple imperative programs by Maillard et al. [
70]. We believe that, with some additional work, their ideas could be used to also support unbounded loops in SSProve (see Section
5.5 for details).
The
uniform rule relates sampling from uniform distributions on finite sets
A and
B that are in a bijective correspondence. Note how it applies the bijection
f in the continuation on the right-hand side:
The code
\(y \mathbin {\mathtt {\lt \$}}D \: ;\:\,c_0\) samples
y from the subdistribution
D. If
y is never used in
\(c_0\) , as indicated by the last premise of the
dead-sample rule, then we would like to argue that the sampling constitutes “dead code” and can be ignored. This intuition only holds if
D is a proper distribution rather than a subdistribution. For instance, if
D is the null distribution, the sampling behaves like “
\(\texttt {assert false}\) ” and can certainly not be ignored. The premise
\(\sum _{x \in |D|} D(x) = 1\) ensures that
D is indeed a proper distribution (also known as a “lossless subdistribution”). A uniform distribution over a non-empty set would, for instance, constitute a proper distribution in this sense:
The
sample-irrelevant rule has a similar flavor to
dead-sample, as it too requires
D to be a proper distribution. We assume that
\(c_0\ y\) can be related to
\(c_1\) for all values of
y. In other words, the choice of a particular value for
y is irrelevant for the pre- and postcondition at hand. Therefore, sampling
y from a proper distribution
D will likewise allow us to conclude that
\(c_0\ y\) is related to
\(c_1\) :
The
assert rule relates two
assert commands, as long as “
\(b_0 = b_1\) ” holds before the commands. Note that while the precondition is a predicate on initial memories, nothing prevents it form talking about other things such as the Booleans
\(b_0\) and
\(b_1\) quantified at the meta-level. It guarantees “
\(b_0 = \mathtt {true}\wedge b_1 = \mathtt {true}\) ” afterwards, ignoring the values
\(a_0\) and
\(a_1\) of type
\(\texttt {unit}\) :
The one-sided
assertL rule specifies the behavior an
assert with a
true Boolean, by relating it with
return (). Note that if a code fragment
\(c_0\) is shown to be related to an assertion failure
, then
\(c_0\) must necessarily contain an assertion failure as well, i.e., correspond to the null sub-distribution. Indeed the (sound) model of our program logic, explained in Section
5, gives rise to a total correctness semantics [
70] for assertion failures: assertion failures only relate to other assertion failures:
The
assertD rule allows reasoning about the dependent version of
assert where the continuation
\(κ _i\) is only well-defined if the assertion holds, as described in Section
3.1. As in the
assert rule, the two assertion conditions
\(b_0\) and
\(b_1\) may
a priori be different. The precondition
\(\phi\) has to ensure that
\(b_0\) and
\(b_1\) are either both true or both false. The continuations
\(κ _i\) are defined only in case the assertions succeed. Under this assumption, here represented as the hypotheses
\(H_0\) and
\(H_1\) , the continuations
\(κ _i\) must be related for the same
pre and
post as the composite statements “
\({\texttt {assert } b_i \texttt { as } h_i} \: ;\:\,κ _i\ h_i\) ”. The intuition for the validity of this rule is the following: if
\(b_i\) is true,
\({\texttt {assert } b_i \texttt { as } h_i}\) is defined as
\(κ _i \ h_i\) , and we appeal to the last premise. If
\(b_i\) is false, then both composite statements
\(\texttt {fail}\) and evaluate to the null distribution:
The
put-get rule states that looking up the value at location
\(ℓ\) after storing
v at
\(ℓ\) results in the value
v. We also have a similar rule to remove a
put right before another one at the same location, and one for two
get in a row. More interestingly, we provide one-sided rules for
get and
put, which update the pre- or postcondition accordingly:
More generally, we define handy tactics to apply these rules immediately, as well as performing the necessary massaging of goals so that they become applicable. As such, we have automation for swapping multiple lines at once and checking that the swap was legal. Moreover, these tactics rely on the hints mechanism of Coq and can thus be extended by the user.