\documentclass[11pt,twoside]{scrartcl}
%opening
\newcommand{\lecid}{15-414}
\newcommand{\leccourse}{Bug Catching: Automated Program Verification}
\newcommand{\lecdate}{} %e.g. {October 21, 2013}
\newcommand{\lecnum}{7}
\newcommand{\lectitle}{Programs with Arrays}
\newcommand{\lecturer}{Matt Fredrikson}
\usepackage{lecnotes}
\usepackage[irlabel]{bugcatch}
\usepackage{xcolor}
\usepackage{listings}
\definecolor{mygray}{rgb}{0.5,0.5,0.5}
\definecolor{backgray}{gray}{0.95}
\lstdefinestyle{customjava}{
belowcaptionskip=1\baselineskip,
breaklines=true,
language=Java,
showstringspaces=false,
numbers=left,
xleftmargin=2em,
framexleftmargin=1.5em,
numbersep=5pt,
numberstyle=\tiny\color{mygray},
basicstyle=\footnotesize\ttfamily,
keywordstyle=\color{blue},
commentstyle=\itshape\color{purple!40!black},
tabsize=2,
backgroundcolor=\color{backgray},
escapechar=\%
}
\begin{document}
\maketitle
\thispagestyle{empty}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Introduction}
The previous lecture focused on loops, starting with axioms and leading to a derived rule that allows us to simplify reasoning about loops to reasoning about the behavior of a single iteration of their bodies. We worked an example involving a program that uses loops to compute the square of a number, and found that much of the difficulty in reasoning about loops lies in finding a suitable invariant.
Loops are frequently used to compute over a sort of programming element that we haven't introduced yet, namely arrays. Arrays are an important data structure in imperative programming, enabling us to implement things that we couldn't otherwise. However, they introduce significant complexity into programs' behavior, so sound reasoning about their use is crucial. Today we will introduce arrays into our language, and use our semantics to argue the correctness of the binary search code we discussed in the first lecture. Along the way, we will build more experience in finding suitable loop invariants for increasingly complex code, and in all likelihood, learn to appreciate the benefits of using an SMT solver to complete our proofs.
%%%%%%%%%%%%%%%%%%%%
\section{Recall: Loop Invariants}
Last lecture we derived the loop rule using the dynamic logic sequent calculus. We started from the unwind and unfold axioms,
\begin{calculus}
\cinferenceRule[whileiterateb|$\dibox{\text{unwind}}$]{unfold while loop}
{\linferenceRule[equiv]
{\dbox{\pifs{\ivr}{\plgroup\ausprg;\pwhile{\ivr}{\ausprg}\prgroup}}{\ausfml}}
{\axkey{\dbox{\pwhile{\ivr}{\ausprg}}{\ausfml}}}
}{}%
\cinferenceRule[unfold|$\dibox{\text{unfold}}$]{unfold while loop}
{\linferenceRule[equiv]
{(\ivr\limply\dbox{\ausprg}{\dbox{\pwhile{\ivr}{\ausprg}}{\ausfml}}) \land (\lnot\ivr\limply\ausfml)}
{\axkey{\dbox{\pwhile{\ivr}{\ausprg}}{\ausfml}}}
}{}%
\end{calculus}
The issue with using these axioms to reason about a program is that they do not make our work easier: in either case, they reduce reasoning about a while loop to a bit of logic, and then more reasoning about exactly the same while loop. In principle, as long as we knew how many times a loop would execute in advance, we could use these axioms to unfold the loop body until the condition implies termination. But we will not in general know this information, so we needed different ways of reasoning about loop behavior.
This led to our derivation of the loop rule, which relies on an \emph{invariant} $\inv$.
\[
\cinferenceRule[whileloop|loop]{while loop invariant}
{\linferenceRule[sequent]
{\lsequent[L]{}{\inv}
&
\lsequent[G]{\inv,\ivr}{\dbox{\ausprg}{\inv}}
&
\lsequent[G]{\inv,\lnot\ivr}{\ausfml}}
{\lsequent[L]{}{
\dbox{\pwhile{\ivr}{\ausprg}}{\ausfml}}}
}{}
\]
This rule requires us to prove three things in order to show that $P$ holds after the loop executes.
\begin{enumerate}
\item $\lsequent[L]{}{\inv}$: The loop invariant $\inv$ is true when the loop begins its execution.
\item $\lsequent[G]{\inv,\ivr}{\dbox{\ausprg}{\inv}}$: Assuming that both the loop invariant $\inv$ and the guard $Q$ are true, then executing the body of the loop $\alpha$ one time results in the loop invariant remaining true afterwards.
\item $\lsequent[G]{\inv,\lnot\ivr}{\ausfml}$: Assuming that both the loop invariant $\inv$ and the negation of the guard $\lnot Q$ are true, the postcondition $P$ must also be true.
\end{enumerate}
Certainly, this is an improvement over reasoning from the axioms, becuase we are able to deduce correctness by considering the behavior of just a single execution of the loop body. However, it leaves open the question of how we should go about finding a suitable loop invariant, and this can be nontrivial. Today we'll take a closer look at this question, using a familiar program as an example.
%%%%%%%%%%%%%%%%%%%%
\section{Back to Binary Search}
Let's go back to an example that we briefly touched on in the first lecture. Recall that we looked at a buggy version of binary search, shown below.
\begin{verbatim}
int binarySearch(int key, int[] a, int n) {
int low = 0;
int high = n;
while (low < high) {
int mid = (low + high) / 2;
if(a[mid] == key) return mid; // key found
else if(a[mid] < key) {
low = mid + 1;
} else {
high = mid;
}
}
return -1; // key not found.
}
\end{verbatim}
However, the bug in this case was a bit subtle. If we as the programmer assume that variables assigned type \texttt{int} take values from $\mathbb{Z}$, then there is no bug. Actual computers, being nothing more than glorified finite-state machines, of course do not use integers from $\mathbb{Z}$ but rather machine integers of a fixed size. So when the computer executes \texttt{low + high} on line 6 in the process of determining the midpoint, the resulting sum may be too large to fit into a machine integer and thus overflow, ultimately causing a negative number to be stored in \texttt{mid}.
The simple imperative language that we have studied since does not use machine integers, and we've instead given ourselves the liberty of assuming that all values are integers from $\mathbb{Z}$. This means that if we translate the code from earlier into our language, then we should be able to rigorously prove its correctness using the axioms of Dynamic Logic. However, before we can do so, we will of course need to extend our language to handle arrays.
\paragraph{Basic Arrays: Syntax.}
Most of us are probably familiar with the square bracket notation for expressing arrays, e.g., \texttt{x[i]} to refer to the $i^{th}$ element of the array variable $x$. Because we are already making heavy use of square brackets in dynamic logic formulas, we will instead opt to use parenthesis for array syntax. We must first add a new term $a(\astrm)$ to the syntax, which corresponds to referencing an array $a$ at the index given by $\astrm$. Note that for now we allow arbitrary terms to specify the index, so the following are acceptable terms in the new syntax: $a(0), a(1+x), a(a(x)\cdot 3)$.
However, we do need to be careful about distinguishing between variable symbols and array symbols. We don't want to allow certain sets of terms into the language, like $a + a$ and $a \le a$. To do this, we need to assume that all variable symbols are already defined as either corresponding to arrays or variables.
\[
\begin{array}{llll}
%
\text{term syntax}
&
\astrm,\bstrm ~\bebecomes&
x &(\text{where}~x~\text{is a variable symbol}) \\
&& \alternative
c &(\text{where}~c~\text{is a constant literal}) \\
&& \alternative
a(\astrm) &(\text{where}~a~\text{is an array symbol}) \\
&& \alternative
\astrm+\bstrm & \\
&& \alternative
\astrm\cdot\bstrm &
\end{array}
\]
Note that this definition also prohibits terms like $a(1)(2)$. In a language like C, when looking at this term we might expect that $a$ is an array of arrays, so $a(1)(2)$ obtains the second array stored in $a$, and then looks up its third value. As we will see when we define the semantics, our language does not have arrays of arrays, so we shouldn't allow terms like this into our language. The above syntax definition accomplishes this.
Recall that our syntax for programs specified assignments as taking the form $\pupdate{\pumod{x}{\astrm}}$. This means that in order for programs to update arrays, we must also update program syntax with a new alternative for array update. The term $\pupdate{\pumod{a(\astrm)}}{\bstrm}$ does exactly this.
\[
\begin{array}{llll}
%
\text{program syntax}
&
\asprg,\bsprg ~\bebecomes&
\pupdate{\pumod{x}{\astrm}}&(\text{where}~x~\text{is a variable symbol}) \\
&& \alternative
\pupdate{\pumod{a(\astrm)}}{\bstrm}&(\text{where}~a~\text{is an array symbol}) \\
&& \alternative
\ptest{\ivr} & \\
&& \alternative
\pif{\ivr}{\asprg}{\bsprg} & \\
&& \alternative
\asprg;\bsprg & \\
&& \alternative
\pwhile{\ivr}{\asprg}
\end{array}
\]
Now that there are arrays in our language, we can write the binary search program in it. We don't have procedures and \texttt{return} statements in our language, but we can change things a bit to make it work.
\[
\begin{array}{l}
l := 0;
h := n; \\
m := (l+h) / 2; \\
\pwhile{l < h \land a(m) \ne k}{\{\\
\ \ \ \ \pif{a(m) < k}{\\
\ \ \ \ \ \ \ \ l := m + 1; \\
\ \ \ \ }{\\
\ \ \ \ \ \ \ \ h := m; \\}
\ \ \ \ m := (l + h) / 2; \\
\}
} \\
\pif{l < h}{r := m}{r := -1}
\end{array}
\]
\paragraph{Basic Arrays: Term Semantics.}
Now that we've defined array syntax, we need to give them semantics so that we can reason about their use in programs. Intuitively, we think of arrays as functions from the integers to the type of element stored in the array. We can generalize this idea further by observing that multi-dimensional arrays are nothing more than functions with arity greater than one, from the integers to the element type. This way of modeling generalized array values even extends nicely to constant values, because we can view them as functions of arity zero.
Making this more precise, we will update our semantics for terms and programs to account for all values (i.e., both arrays and integers) as functions of the appropriate arity. We won't worry about multi-dimensional arrays for now (this could return as an exercise), so all of the values we work with will be functions of arity 0 (i.e., integer constants) or 1 (i.e., arrays storing integers). To make this change, we start with redefining the set of all states $\mathcal{S}$. We had previously defined $\mathcal{S}$ as a function that assigns an integer value in $\mathbb{Z}$ to every variable in $V$, the set of all variables. Now we will define $\mathcal{S}$ to be a function that maps $V$ to the set of functions over $\mathbb{Z}$ of arity at most 1:
\[
\mathcal{S} = (\mathbb{Z}^{0} \mapsto \mathbb{Z}) \cup (\mathbb{Z}^{1} \mapsto \mathbb{Z}) = \mathbb{Z} \cup \mathbb{Z} \mapsto \mathbb{Z}
\]
Note that with this change, we can still view the semantics of terms $\omega\llbracket\astrm\rrbracket$ as a subset of $\mathcal{S}$, and that of programs $\llbracket\asprg\rrbracket$ as a subset of $\mathcal{S}\times\mathcal{S}$.
We can now define the semantics of terms with arrays as we have done previously, by inductively distinguishing the shape of a given term $\astrm$ against a state $\omega \in \mathcal{S}$. However, it might first be a good idea to consider how we would like arrays to be used in the language. For instance, do we want to assign meaning to programs that contain constructs like $a_1 + a_2$? Probably not, so we will need to be careful in how we define the semantics to avoid such cases. Intuitively, we will do so by only assigning semantics to terms involving arrays that evaluate to integer constants. While this will prohibit some cases that we might see as useful, such as $a_1 := a_2$ (i.e., copy an entire array) and $a_1 = a_2$ (i.e., compare all elements of two arrays), we can implement such functionality in other ways.
\begin{definition}[Semantics of terms with basic arrays] The semantics of a term $\astrm$ in a state $\omega \in \mathcal{S}$ is its value $\omega\llbracket\astrm\rrbracket$, defined inductively as follows.
\begin{itemize}
\item $\omega\llbracket c \rrbracket = c$ for number literals $c$
\item $\omega\llbracket x \rrbracket = \omega(x)$
\item $\omega\llbracket a(\astrm) \rrbracket = \omega(a)(\omega\llbracket\astrm\rrbracket)$
\item $\omega\llbracket \astrm + \bstrm \rrbracket = \omega\llbracket \astrm\rrbracket + \omega\llbracket\bstrm \rrbracket$
\item $\omega\llbracket \astrm \cdot \bstrm \rrbracket = \omega\llbracket \astrm\rrbracket \cdot \omega\llbracket\bstrm \rrbracket$
\end{itemize}
\end{definition}
The only change is the addition of the term $a(\astrm)$, which corresponds to array lookup. The semantics defines this by looking up $a$ in $\omega$, evaluating $\astrm$ in $\omega$, and then applying the results.
\section{Proving Binary Search}
Now that we have semantics for terms that mention arrays, we should be able to reason about the correctness of the binary search code from earlier. But what should the specification be?
\paragraph{Specification.}
As for a precondition, there is one big assumption that this code must make, namely that the array $a$ is already sorted. There are several ways to specify sortedness of arrays, and they all involve quantifiers. Perhaps the most obvious specification would simply state that for all valid positions in the array $0 \le i < n$, every pair $a(i-1)$ and $a(i)$ are in order:
\[
\forall i . 0 < i < n \rightarrow a(i-1) \le a(i)
\]
This specification is fine, but we should think about how we might want to use it later on. In a sequent calculus proof, this precondition will eventually give us $0 < i < n \rightarrow a(i-1) \le a(i)$ (for some $i$) in the antecedent. This will let us conclude things directly about adjacent elements in the array, but if we want to reason about elements that are arbitrarily far away, e.g., to prove that $a(0) \le a(n-1)$, then we will have to do a bit more work as this fact is not immediate from the precondition. We would need to prove a lemma:
\[
\vdash (\forall i . 0 < i < n \rightarrow a(i-1) \le a(i)) \rightarrow (\forall i_1, i_2 . 0 \le i_1 \le i_2 < n \rightarrow a(i_1) \le a(i_2))
\]
However, because we are free to place whatever we would like in the precondition (within reason), we can simply use the formula on the right as our precondition for sortedness.
\[
\keywordfont{sorted}(a,n) \equiv \forall i_1, i_2 . 0 \le i_1 \le i_2 < n \rightarrow a(i_1) \le a(i_2)
\]
Implicit in this is another precondition, namely that $0 < n$. If we did not have this, then someone could initialize $n$ to be negative, which would cause $\keywordfont{sorted}$ to evaluate to true but result in meaningless program behavior. So, our precondition is:
\[
\keywordfont{pre}(a,n) \equiv 0 < n \land \keywordfont{sorted}(a,n)
\]
Now we need a postcondition. Looking at the program text, the variable $r$ is used to store the result on the last line. If $l < h$, which means that the loop ended early after finding an element with value $k$, then $r$ takes the position of the element. Otherwise, $r$ takes the value $-1$. This suggests a postcondition with two cases:
\[
\begin{array}{ll}
0 \le r \rightarrow a(r) = k & k~\text{found at position}~r \\
r < 0 \rightarrow \forall i . 0 \le i < n \rightarrow a(i) \ne k & k~\text{not found}
\end{array}
\]
The antecedents are mutually exclusive, so we can simply conjoin these cases to arrive at our postcondition:
\[
\keywordfont{post}(a,r,k,n) \equiv (0 \le r \rightarrow a(r) = k) \land (r < 0 \rightarrow \forall i . 0 \le i < n \rightarrow a(i) \ne k)
\]
The dynamic logic formula that we would then like to prove is:
\[
\begin{array}{rl}
%
\keywordfont{pre}(a,n) \rightarrow [& \\
&
\left.
\begin{array}{l}
%
\left.
\begin{array}{l}
%
l := 0; \\
h := n; \\
m := (l+h) / 2;
\end{array}
\ \ \ \ \ \ \ \ \right\} \gamma
\\
\pwhile{l < h \land a(m) \ne k}{\{\\
\left.
\begin{array}{rl}
%
& \ \ \ \ \pif{a(m) < k}{\\
& \ \ \ \ \ \ \ \ l := m + 1; \\
& \ \ \ \ }{\\
& \ \ \ \ \ \ \ \ h := m; \\}
& \ \ \ \ m := (l + h) / 2;
\end{array} \right\} \beta
\\
\}} \\
\pif{l < h}{r := m}{r := -1}
\end{array}
\right\} \alpha
\\
] & \keywordfont{post}(a,r,k,n)
\end{array}
\]
Notice that we will use $\alpha$ as shorthand for the entire program, and $\beta$ for the portion within the loop, and $\gamma$ for the first three assignments.
\paragraph{Finding a loop invariant.}
Before we can begin proving this formula valid, we need to think about a loop invariant. What should it be? It isn't immediately clear from inspection, so perhaps we can find a somewhat systematic way to nudge us in the right direction. One approach that often works is to start writing the proof with a placeholder for the loop invariant, so that we can see what is needed of the loop invariant to make the proof work.
\begin{sequentdeduction}[array]
\linfer[implyr] {
\linfer[composeb] {
\linfer[ifb] {
\lsequent{\mathtt{pre}(a,n)} {{}[\gamma][\pwhile{lk} {{}\mathtt{mbound}(l,m,(l+m)/2,n) \land \mathtt{notfound}(a,n,k,l,m) \land m \le n \land \mathtt{sorted}}
}
} {
\lsequent{\inv,lk} {{}[h := m]\inv(a,l,m,(l+h)/2,n)}
}
\end{sequentdeduction}
As in the previous branch, because $a$ and $n$ aren't updated, the proof of \textcircled{p} will be similarly trivial. The proof of \textcircled{o}, which is preservation of $h \le n$, is not as trivial this time. Because $h$ was updated to $m$, we need to show that:
\[
\begin{array}{ll}
%
& l < h \rightarrow 0 \le m < n,
\forall i . 0 \le i < n \rightarrow a(i) = k \rightarrow l \le i < h, \\
& h \le n,
\forall i_1, i_2 . 0 \le i_1 \le i_2 < n \rightarrow a(i_1) \le a(i_2), \\
& l < h,
a(m) > k
\end{array}
\vdash
m \le n
\]
This follows from applying $\rightarrow$L on $l < h \rightarrow 0 \le m < n$, and the fact that $l < h$ is one of the premises. However, it should be clear that manually proving all of the obligations is a very laborious undertaking! Once the correct loop invariant has been identified, then the rest of the work is difficult only insofar as it requires a lot of work. This is exactly why we use SMT solvers to prove straightforward formulas like the one we just discussed, and verification condition generators to apply the axioms of dynamic logic to eliminate box and diamond terms.
The remaining obligations \textcircled{m} and \textcircled{n} are left as an exercise, as is the proof that the first three statements in $\gamma$ establish the invariant, $\lsequent{\mathtt{pre}(a,n)}{{}[\gamma] J}$.
\end{document}