240 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			240 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| 
								 | 
							
								---
							 | 
						||
| 
								 | 
							
								title: "Implementing and Verifying \"Static Program Analysis\" in Agda, Part 4: The Fixed-Point Algorithm"
							 | 
						||
| 
								 | 
							
								series: "Static Program Analysis in Agda"
							 | 
						||
| 
								 | 
							
								description: "In this post, I give a top-level overview of my work on formally verified static analyses"
							 | 
						||
| 
								 | 
							
								date: 2024-08-10T17:37:42-07:00
							 | 
						||
| 
								 | 
							
								tags: ["Agda", "Programming Languages"]
							 | 
						||
| 
								 | 
							
								draft: true
							 | 
						||
| 
								 | 
							
								---
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								In the preivous post we looked at lattices of finite height, which are a crucial
							 | 
						||
| 
								 | 
							
								ingredient to our static analyses. In this post, I will describe the specific
							 | 
						||
| 
								 | 
							
								algorithm that makes use of these lattices that will be at the core of it all.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Lattice-based static analyses tend to operate by iteratively combining facts
							 | 
						||
| 
								 | 
							
								from the program into new ones. For instance, when analyzing `y = 1 + 2`, we
							 | 
						||
| 
								 | 
							
								take the (trivial) facts that the numbers one and two are positive, and combine
							 | 
						||
| 
								 | 
							
								them into the knowledge that `y` is positive as well. If another line of code
							 | 
						||
| 
								 | 
							
								reads `x = y + 1`, we then apply our new knowledge of `y`  to determine the sign
							 | 
						||
| 
								 | 
							
								of `x`, too. Combining facs in this manner gives us more information, which we
							 | 
						||
| 
								 | 
							
								can then continue to apply to learn more about the program.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								A static program analyzer, however, is a very practical thing. Although in
							 | 
						||
| 
								 | 
							
								mathemaitics we may allow ourselves to delve into infinite algorithms, we have
							 | 
						||
| 
								 | 
							
								no such luxury while trying to, say, compile some code. As a result, after
							 | 
						||
| 
								 | 
							
								a certain point, we need to stop our iterative (re)combination of facts. In
							 | 
						||
| 
								 | 
							
								an ideal world, that point would be when we know we have found out everything we
							 | 
						||
| 
								 | 
							
								could about the program. A corollary to that would be that this point must
							 | 
						||
| 
								 | 
							
								be guaranteed to eventually occur, lest we keep looking for it indenfinitely.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The fixed-point algorithm does this for us. If we describe our analysis as
							 | 
						||
| 
								 | 
							
								a monotonic function over a finite-height lattice, this algorithm gives
							 | 
						||
| 
								 | 
							
								us a surefire way to find out facts about our program that constitute "complete"
							 | 
						||
| 
								 | 
							
								information that can't be re-inspected to find out more. The algorithm is guaranteed
							 | 
						||
| 
								 | 
							
								to terminate, which means that we will not get stuck in an infinite loop.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								### The Algorithm
							 | 
						||
| 
								 | 
							
								Take a lattice \(L\) and a monotonic function \(f\). We've
							 | 
						||
| 
								 | 
							
								[talked about monotonicity before]({{< relref "01_spa_agda_lattices#define-monotonicity" >}}),
							 | 
						||
| 
								 | 
							
								but it's easy to re-state. Specifically, a function is monotonic if the following
							 | 
						||
| 
								 | 
							
								rule holds true:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< latex >}}
							 | 
						||
| 
								 | 
							
								\textbf{if}\ a \le b\ \textbf{then}\ f(a) \le f(b)
							 | 
						||
| 
								 | 
							
								{{< /latex >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Recall that the less-than relation on lattices in our case
							 | 
						||
| 
								 | 
							
								[encodes specificity]({{< relref "01_spa_agda_lattices#specificity" >}}).
							 | 
						||
| 
								 | 
							
								In particular, if elements of our lattice describe our program, than smaller
							 | 
						||
| 
								 | 
							
								elements should provide more precise descriptions (where "`x` is potive"
							 | 
						||
| 
								 | 
							
								is more precise than "`x` has any sign", for example). Viewed through this
							 | 
						||
| 
								 | 
							
								lens, monotonicity means that more specific inputs produce more specific outputs.
							 | 
						||
| 
								 | 
							
								That seems reasonable.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Now, let's start with the least element of our lattice, denoted \(\bot\).
							 | 
						||
| 
								 | 
							
								A lattice of finite height is guaranteed to have such an element. If it didn't,
							 | 
						||
| 
								 | 
							
								we could always extend chains by tacking on a smaller element to their bottom,
							 | 
						||
| 
								 | 
							
								and then the lattice wouldn't have a finite height anymore.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Now, apply \(f\) to \(\bot\) to get \(f(\bot)\). Since \(\bot\) is the least
							 | 
						||
| 
								 | 
							
								element, it must be true that \(\bot \le f(\bot)\). Now, if it's "less than or equal",
							 | 
						||
| 
								 | 
							
								is it "less than", or is "equal")? If it's the latter, we have \(\bot = f(\bot)\).
							 | 
						||
| 
								 | 
							
								This means we've found a fixed point: given our input \(\bot\) our analysis \(f\)
							 | 
						||
| 
								 | 
							
								produced no new information, and we're done. Otherwise, we are not done, but we
							 | 
						||
| 
								 | 
							
								know that \(\bot < f(\bot)\), which will be helpful shortly.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Continuing the "less than" case, we can apply \(f\) again, this time to \(f(\bot)\).
							 | 
						||
| 
								 | 
							
								This gives us \(f(f(\bot))\). Since \(f\) is monotonic and \(\bot \le f(\bot)\), we know
							 | 
						||
| 
								 | 
							
								also that \(f(\bot) \le f(f(\bot))\). Again, ask "which is it?", and as before, if
							 | 
						||
| 
								 | 
							
								\(f(\bot) = f(f(\bot))\), we have found a fixed point. Otherwise, we know that
							 | 
						||
| 
								 | 
							
								\(f(\bot) < f(f(\bot))\).
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								We can keep doing this. Notice that with each step, we are either done
							 | 
						||
| 
								 | 
							
								(having found a fixed point) or we have a new inequality in our hands. We can
							 | 
						||
| 
								 | 
							
								arrange the ones we've seen so far into a chain:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< latex >}}
							 | 
						||
| 
								 | 
							
								\bot < f(\bot) < f(f(\bot)) < ...
							 | 
						||
| 
								 | 
							
								{{< /latex >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Each time we fail to find a fixed point, we add one element to our chain, growing
							 | 
						||
| 
								 | 
							
								it. But if our lattice \(L\) has a finite height, that means eventually this
							 | 
						||
| 
								 | 
							
								process will have to stop; the chain can't grow forever; eventually, we will
							 | 
						||
| 
								 | 
							
								have to find a value such that \(v = f(v)\). Thus, our algorithm is guaranteed
							 | 
						||
| 
								 | 
							
								to terminate, and give a fixed point.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								I implemented the iterative process of applying \(f\) using a recursive function.
							 | 
						||
| 
								 | 
							
								Agda has a termination checker, to which the logic above --- which proves that
							 | 
						||
| 
								 | 
							
								iteration will eventually finish --- is not at all obvious. The trick to
							 | 
						||
| 
								 | 
							
								getting it to work was to use a notion of "gas": an always-decreasing value
							 | 
						||
| 
								 | 
							
								that serves as one of the functions' arguments. Since the value is always decreasing
							 | 
						||
| 
								 | 
							
								in size, the termination checker is satisfied.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This works by observing that we already have a rough idea of the maximum number
							 | 
						||
| 
								 | 
							
								of times our function will recurse; that would be the height of the lattice.
							 | 
						||
| 
								 | 
							
								After that, we would be building an impossibly long chain. So, we'll give the
							 | 
						||
| 
								 | 
							
								function a "budget" of that many iterations, plus one more. Since the chain
							 | 
						||
| 
								 | 
							
								increases once each time the budget shrinks (indicating recursion), running
							 | 
						||
| 
								 | 
							
								out of our "gas" will mean that we built an impossibly long chain --- it
							 | 
						||
| 
								 | 
							
								will provably never happen.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								In all, the recursive function is as follows:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< codelines "Agda" "agda-spa/Fixedpoint.agda" 53 64 >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The first case handles running out of gas, arguing by bottom-elimination (contradiction).
							 | 
						||
| 
								 | 
							
								The second case follows the algorithm I've described pretty closely; it
							 | 
						||
| 
								 | 
							
								applies \(f\) to an existing value, checks if the result is equal (equivalent)
							 | 
						||
| 
								 | 
							
								to the original, and if it isn't, it grows the existing chain of elements
							 | 
						||
| 
								 | 
							
								and invokes the step function rescurisvely with the grown chain and less gas.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The recursive function implements a single "step" of the process (applying `f`,
							 | 
						||
| 
								 | 
							
								comparing for equality, returning the fixed point if one was found). All that's
							 | 
						||
| 
								 | 
							
								left is to kick off the process using \(\bot\). This is what `fix` does:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< codelines "Agda" "agda-spa/Fixedpoint.agda" 66 67 >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This functions is responsible for providing gas to `doStep`; as I mentioned above,
							 | 
						||
| 
								 | 
							
								it provides just a bit more gas than the maximum-length chain, which means that
							 | 
						||
| 
								 | 
							
								if the gas is exhausted, we've certainly arrived at a contradiction. It also
							 | 
						||
| 
								 | 
							
								provides an initial chain onto which `doStep` will keep tacking on new inequalities
							 | 
						||
| 
								 | 
							
								as it finds them. Since we haven't found any yet, this is the single-element
							 | 
						||
| 
								 | 
							
								chain of \(\bot\). The last thing is does is set up the reursion invariant
							 | 
						||
| 
								 | 
							
								(that the sum of the gas and the chain length is constant), and provides
							 | 
						||
| 
								 | 
							
								a proof that \(\bot \le f(\bot)\). This function always returns a fixed point.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								### Least Fixed Point
							 | 
						||
| 
								 | 
							
								Functions can have many fixed points. Take the identity function that simply
							 | 
						||
| 
								 | 
							
								returns its argument unchanged; this function has a fixed point for every
							 | 
						||
| 
								 | 
							
								element in its domain, since, for example, \(\text{id}(1) = 1\), \(\text{id}(2) = 2\), etc.
							 | 
						||
| 
								 | 
							
								The fixed point found by our algorithm above is somewhat special among the
							 | 
						||
| 
								 | 
							
								possible fixed points of \(f\): it is the _least fixed point_ of the function.
							 | 
						||
| 
								 | 
							
								Call our fixed point \(a\); if there's another point \(b\) such that \(b=f(b)\),
							 | 
						||
| 
								 | 
							
								then the fixed point we found must be less than or equal to \(b\) (that is,
							 | 
						||
| 
								 | 
							
								\(a \le b\)). This is important given our interpretation of "less than" as "more specific":
							 | 
						||
| 
								 | 
							
								the fixedpoint algorithm produces the most specific possible information about
							 | 
						||
| 
								 | 
							
								our program given the rules of our analysis.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The proof is simple; suppose that it took \(k\) iterations of calling \(f\)
							 | 
						||
| 
								 | 
							
								to arrive at our fixed point. This gives us:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< latex >}}
							 | 
						||
| 
								 | 
							
								a = \underbrace{f(f(...(f(}_{k\ \text{times}}\bot)))) = f^k(\bot)
							 | 
						||
| 
								 | 
							
								{{< /latex >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Now, take our other fixed point \(b\). Since \(\bot\) is the least element of
							 | 
						||
| 
								 | 
							
								the lattice, we have \(\bot \le b\).
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< latex >}}
							 | 
						||
| 
								 | 
							
								\begin{array}{ccccccccr}
							 | 
						||
| 
								 | 
							
								& & \bot    & \le &      &   & b \quad \implies & \text{(monotonicity of $f$)}\\
							 | 
						||
| 
								 | 
							
								& & f(\bot) & \le & f(b) & = & b \quad \implies & \text{($b$ is a fixed point, monotonicity of $f$)}\\
							 | 
						||
| 
								 | 
							
								& & f^2(\bot) & \le & f(b) & = & b \quad \implies & \text{($b$ is a fixed point, monotonicity of $f$)}\\
							 | 
						||
| 
								 | 
							
								\\
							 | 
						||
| 
								 | 
							
								& & \vdots & & \vdots & & \vdots & \\
							 | 
						||
| 
								 | 
							
								\\
							 | 
						||
| 
								 | 
							
								a & = & f^k(\bot) & \le & f(b) & = & b &
							 | 
						||
| 
								 | 
							
								\end{array}
							 | 
						||
| 
								 | 
							
								{{< /latex >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Because of the monotonicity of \(f\), each time we apply it, it preserves the
							 | 
						||
| 
								 | 
							
								less-than relationship that started with \(\bot \le b\). Doing that \(k\) times,
							 | 
						||
| 
								 | 
							
								we verify that \(a\) is our fixed point.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								To convince Agda of this proof, we once again get in an argument with the termination
							 | 
						||
| 
								 | 
							
								checker, which ends the same way it did last time: with us using the notion of 'gas'
							 | 
						||
| 
								 | 
							
								to ensure that the repeated application of \(f\) eventually ends. Since we're
							 | 
						||
| 
								 | 
							
								interested in verifying that `doStep` producdes the least fixed point, we formulate
							 | 
						||
| 
								 | 
							
								the proof in terms of `doStep` applied to various arguments.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< codelines "Agda" "agda-spa/Fixedpoint.agda" 76 84 >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								As with `doStep`, this function takes as arguments the amount of gas `g` and
							 | 
						||
| 
								 | 
							
								a partially-built chain `c`, which gets appended to for each failed equality
							 | 
						||
| 
								 | 
							
								comparison. In addition, however, this function takes another arbitrary fixed
							 | 
						||
| 
								 | 
							
								point `b`, which is greater than the current input to `doStep` (which is
							 | 
						||
| 
								 | 
							
								a value \(f^i(\bot\)\) for some \(i\)). It then proves that when `doStep` terminates
							 | 
						||
| 
								 | 
							
								(which will be with a value in the form \(f^k(\bot)\)), this value will still
							 | 
						||
| 
								 | 
							
								be smaller than `b`. Since it is a proof about `doStep`, `stepPreservesLess`
							 | 
						||
| 
								 | 
							
								proceeds by the same case analysis as its subject, and has a very similar (albeit
							 | 
						||
| 
								 | 
							
								simpler) structure. In short, though, it encodes the relatively informal proof
							 | 
						||
| 
								 | 
							
								I gave above.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Just like with `doStep`, I define a helper function for `stepPreservesLess` that
							 | 
						||
| 
								 | 
							
								kicks off its recursive invocations.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< codelines "Agda" "agda-spa/Fixedpoint.agda" 86 87 >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Above, `aᶠ` is the output of `fix`:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< codelines "Agda" "agda-spa/Fixedpoint.agda" 69 70 >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								### What is a Program?
							 | 
						||
| 
								 | 
							
								With the fixed point algorithm in hand, we have all the tools we need to define
							 | 
						||
| 
								 | 
							
								static program analyses:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								1. We've created a collection of "lattice builders", which allow us to combine
							 | 
						||
| 
								 | 
							
								   various lattice building blocks into more complicated structures; these
							 | 
						||
| 
								 | 
							
								   structures are advanced enough to represent the information about our programs.
							 | 
						||
| 
								 | 
							
								2. We've figured out a way (our fixed point algorithm) to repeatedly apply
							 | 
						||
| 
								 | 
							
								   an inference function to our programs and eventually produce results.
							 | 
						||
| 
								 | 
							
								   This algorithm requires some additional properties from our latttices.
							 | 
						||
| 
								 | 
							
								3. We've proven that our lattice builders create lattices with these properties,
							 | 
						||
| 
								 | 
							
								   making it possible to use them to construct functions fit for our fixed point
							 | 
						||
| 
								 | 
							
								   algorithm.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								All that's left is to start defining monotonic functions over lattices! Except,
							 | 
						||
| 
								 | 
							
								what are we analyzing? We've focused a fair bit on the theory of lattices,
							 | 
						||
| 
								 | 
							
								but we haven't yet defined even a tiny piece of the language that our programs
							 | 
						||
| 
								 | 
							
								will be analyzing. We will start with programs like this:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								```
							 | 
						||
| 
								 | 
							
								x = 42
							 | 
						||
| 
								 | 
							
								y = 1
							 | 
						||
| 
								 | 
							
								if y {
							 | 
						||
| 
								 | 
							
								  x = -1;
							 | 
						||
| 
								 | 
							
								} else {
							 | 
						||
| 
								 | 
							
								  x = -2;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								```
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								We will need to model these programs in Agda by describing them as trees
							 | 
						||
| 
								 | 
							
								([Abstract Syntax Trees](https://en.wikipedia.org/wiki/Abstract_syntax_tree), to be
							 | 
						||
| 
								 | 
							
								precise). We will also need to specify how to evaluate these programs (provide
							 | 
						||
| 
								 | 
							
								the [semantics](https://en.wikipedia.org/wiki/Semantics_(computer_science)) of
							 | 
						||
| 
								 | 
							
								our language). We will use big-step (also known as "natural") operational semantics
							 | 
						||
| 
								 | 
							
								to do so; here's an example rule:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								{{< latex >}}
							 | 
						||
| 
								 | 
							
								\frac{\rho_1, e \Downarrow z \quad \neg (z = 0) \quad \rho_1,s_1 \Downarrow \rho_2}
							 | 
						||
| 
								 | 
							
								     {\rho_1, \textbf{if}\ e\ \textbf{then}\ s_1\ \textbf{else}\ s_2\ \Downarrow\ \rho_2}
							 | 
						||
| 
								 | 
							
								{{< /latex >}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The above reads:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								> If the condition of an `if`-`else` statement evaluates to a nonzero value,
							 | 
						||
| 
								 | 
							
								> then to evaluate the statement, you evaluate its `then` branch.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								In the next post, we'll talk more about how these rules work, and define
							 | 
						||
| 
								 | 
							
								the remainder of them to give our language life. See you then!
							 |