blog-static/content/blog/08_spa_agda_forward/index.md
Danila Fedorin 3b9c2edcdd Write up the "verified" portion of the forward analysis
Signed-off-by: Danila Fedorin <danila.fedorin@gmail.com>
2024-12-25 19:03:51 -08:00

22 KiB
Raw Blame History

title series description date tags draft
Implementing and Verifying "Static Program Analysis" in Agda, Part 8: Forward Analysis Static Program Analysis in Agda In this post, I use the monotone lattice framework and verified CFGs to define a sign analysis 2024-12-01T15:09:07-08:00
Agda
Programming Languages
true

In the previous post, I showed that the Control Flow graphs we built of our programs match how they are really executed. This means that we can rely on these graphs to compute program information. In this post, we finally get to compute that information. Here's a quick bit paraphrasing from last time that provides a summary of our approach:

  1. We will construct a finite-height lattice. Every single element of this lattice will contain information about each variable at each node in the Control Flow Graph.
  2. We will then define a monotonic function that update this information using the structure encoded in the CFGs edges and nodes.
  3. Then, using the fixed-point algorithm, we will find the least element of the lattice, which will give us a precise description of all program variables at all points in the program.
  4. Because we have just validated our CFGs to be faithful to the languages semantics, well be able to prove that our algorithm produces accurate results.

Let's jump right into it!

Choosing a Lattice

A lot of this time, we have been [talking about lattices]({{< relref "01_spa_agda_lattices" >}}), particularly [lattices of finite height]({{< relref "03_spa_agda_fixed_height" >}}). These structures represent things we know about the program, and provide operators like (\sqcup) and (\sqcap) that help us combine such knowledge.

The forward analysis code I present here will work with any finite-height lattice, with the additional constraint that equivalence of lattices is decidable, which comes from [the implementation of the fixed-point algorithm]({{< relref "04_spa_agda_fixedpoint" >}}), in which we routinely check if a function's output is the same as its input.

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 4 8 >}}

The finite-height lattice L is intended to describe the state of a single variable. One example of a lattice that can be used as L is our sign lattice. We've been using the sign lattice in our examples [from the very beginning]({{< relref "01_spa_agda_lattices#lattices" >}}), and we will stick with it for the purposes of this explanation. However, this lattice alone does not describe our program, since it only talks about a single sign; programs have lots of variables, all of which can have different signs! So, we might go one step further and define a map lattice from variables to their signs:

{{< latex >}} \text{Variable} \to \text{Sign} {{< /latex >}}

We [have seen]({{< relref "02_spa_agda_combining_lattices#the-map-lattice" >}}) that we can turn any lattice L into a map lattice A \to L, for any type of keys A. In this case, we will define A \triangleq \text{Variable}, and L \triangleq \text{Sign}. The [sign lattice has a finite height]({{< relref "02_spa_agda_combining_lattices#the-map-lattice" >}}), and I've proven that, as long as we pick a finite set of keys, [map lattices A \to L have a finite height if L has a finite height]({{< relref "03_spa_agda_fixed_height#fixed-height-of-the-map-lattice" >}}). Since a program's text is finite, \text{Variable} is a finite set, and we have ourselves a finite-height lattice \text{Variable} \to \text{Sign}.

We're on the right track, but even the lattice we have so far is not sufficient. That's because variables have different signs at different points in the program! You might initialize a variable with x = 1, making it positive, and then go on to compute some arbitrary function using loops and conditionals. For each variable, we need to keep track of its sign at various points in the code. When we [defined Control Flow Graphs]({{< relref "06_spa_agda_cfg" >}}), we split our programs into sequences of statements that are guaranteed to execute together --- basic blocks. For our analysis, we'll keep per-variable for each basic block in the program. Since basic blocks are nodes in the Control Flow Graph of our program, our whole lattice will be as follows: {#whole-lattice}

{{< latex >}} \text{Info} \triangleq \text{NodeId} \to (\text{Variable} \to \text{Sign}) {{< /latex >}}

We follow the same logic we just did for the variable-sign lattice; since \text{Variable} \to \text{Sign} is a lattice of finite height, and since \text{NodeId} is a finite set, the whole \text{Info} map will be a lattice with a finite height.

Notice that both the sets of \text{Variable} and \text{NodeId} depend on the program in question. The lattice we use is slightly different for each input program! We can use Agda's parameterized modules to automaitcally parameterize all our functions over programs:

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 36 37 >}}

Now, let's make the informal descriptions above into code, by instantiating our map lattice modules. First, I invoked the code for the smaller variable-sign lattice. This ended up being quite long, so that I could rename variables I brought into scope. I will collapse the relevant code block; suffice to say that I used the suffix v (e.g., renaming _⊔_ to _⊔ᵛ_) for properties and operators to do with variable-sign maps (in Agda: VariableValuesFiniteMap).

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 41 82 "" "(Click here to expand the module uses for variable-sign maps)" >}}

I then used this lattice as an argument to the map module again, to construct the top-level \text{Info} lattice (in Agda: StateVariablesFiniteMap). This also required a fair bit of code, most of it to do with renaming.

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 85 112 "" "(Click here to expand the module uses for the top-level lattice)" >}}

Constructing a Monotone Function

We now have a lattice in hand; the next step is to define a function over this lattice. For us to be able to use the fixed-point algorithm on this function, it will need to be [monotonic]({{< relref "01_spa_agda_lattices#define-monotonicity" >}}).

Our goal with static analysis is to compute information about our program; that's what we want the function to do. When the lattice we're using is the sign lattice, we're trying to determine the signs of each of the variables in various parts of the program. How do we go about this?

Each piece of code in the program might change a variable's sign. For instance, if x has sign 0, and we run the statement x = x - 1, the sign of x will be -. If we have an expression y + z, we can use the signs of y and z to compute the sign of the whole thing. This is a form of abstract interpretation, in which we almost-run the program, but forget some details (e.g., the exact values of x, y, and z, leaving only their signs). The exact details of how this partial evaluation is done are analysis-specific; in general, we simply require an analysis to provide an evaluator. We will define an evaluator for the sign lattice below. {#general-evaluator}

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 166 167 >}}

From this, we know how each statement and basic block will change variables in the function. But we have described them process as "if a variable has sign X, it becomes sign Y" -- how do we know what sign a variable has before the code runs? Fortunately, the Control Flow Graph tells us exactly what code could be executed before any given basic block. Recall that edges in the graph describe all possible jumps that could occur; thus, for any node, the incoming edges describe all possible blocks that can precede it. This is why we spent all that time [defining the predecessors function]({{< relref "06_spa_agda_cfg#additional-functions" >}}).

We proceed as follows: for any given node, find its predecessors. By accessing our \text{Info} map for each predecessor, we can determine our current best guess of variable signs at that point, in the form of a (\text{Variable} \to \text{Sign}) map (more generally, \text{Variable} \to L map in an arbitrary analysis). We know that any of these predecessors could've been the previous point of execution; if a variable x has sign + in one predecessor and (-) in another, it can be either one or the other when we start executing the current block. Early on, we saw that [the (\sqcup) operator models disjunction ("A or B")]({{< relref "01_spa_agda_lattices#lub-glub-or-and" >}}). So, we apply (\sqcup) to the variable-sign maps of all predecessors. The reference Static Program Analysis text calls this operation \text{JOIN}: {#join-preds}

{{< latex >}} \textit{JOIN}(v) = \bigsqcup_{w \in \textit{pred}(v)} \llbracket w \rrbracket {{< /latex >}}

The Agda implementation uses a foldr:

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 139 140 >}}

Computing the "combined incoming states" for any node is a monotonic function. This follows from the monotonicity of (\sqcup) --- in both arguments --- and the definition of foldr.

{{< codelines "agda" "agda-spa/Lattice.agda" 143 151 "" "(Click here to expand the general proof)" >}}

From this, we can formally state that \text{JOIN} is monotonic. Note that the input and output lattices are different: the input lattice is the lattice of variable states at each block, and the output lattice is a single variable-sign map, representing the combined preceding state at a given node.

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 145 149 >}}

Above, the m₁≼m₂⇒m₁[ks]≼m₂[ks] lemma states that for two maps with the same keys, where one map is less than another, all the values for any subset of keys ks are pairwise less than each other (i.e. m₁[k]≼m₂[k], and m₁[l]≼m₂[l], etc.). This follows from the definition of "less than" for maps. {#less-than-lemma}

So those are the two pieces: first, join all the preceding states, then use the abstract interpretation function. I opted to do both of these in bulk:

  1. Take an initial \text{Info} map, and update every basic block's entry to be the join of its predecessors.
  2. In the new joined map, each key now contains the variable state at the beginning of the block; so, apply the abstract interpretation function via eval to each key, computing the state at the end of the block.

I chose to do these in bulk because this way, after each application of the function, we have updated each block with exactly one round of information. The alternative --- which is specified in the reference text --- is to update one key at a time. The difference there is that updates to later keys might be "tainted" by updates to keys that came before them. This is probably fine (and perhaps more efficient, in that it "moves faster"), but it's harder to reason about.

Generalized Update

To implement bulk assignment, I needed to implement the source text's Exercise 4.26:

Exercise 4.26: Recall that f[a \leftarrow x] denotes the function that is identical to f except that it maps a to x. Assume (f : L_1 \to (A \to L_2)) and g : L_1 \to L_2 are monotone functions where L_1 and L_2 are lattices and A is a set, and let a \in A. (Note that the codomain of f is a map lattice.)

Show that the function (h : L_1 \to (A \to L_2)) defined by h(x) = f(x)[a \leftarrow g(x)] is monotone.

In fact, I generalized this statement to update several keys at once, as follows:

{{< latex >}} h(x) = f(x)[a_1 \leftarrow g(a_1, x),\ ...,\ a_n \leftarrow g(a_n, x)] {{< /latex >}}

I called this operation "generalized update".

At first, the exercise may not obviously correspond to the bulk operation I've described. Particularly confusing is the fact that it has two lattices, L_1 and L_2. In fact, the exercise results in a very general theorem; we can exploit a more concrete version of the theorem by setting L_1 \triangleq A \to L_2, resulting in an overall signature for f and h:

{{< latex >}} f : (A \to L_2) \to (A \to L_2) {{< /latex >}}

In other words, if we give the entire operation in Exercise 4.26 a type, it would look like this:

{{< latex >}} \text{ex}{4.26} : \underbrace{K}{\text{value of}\ a} \to \underbrace{(\text{Map} \to V)}{\text{updater}} \to \underbrace{\text{Map} \to \text{Map}}{f} \to \underbrace{\text{Map} \to \text{Map}}_{h} {{< /latex >}}

That's still more general than we need it. This here allows us to modify any map-to-map function by updating a certain key in that function. If we just want to update keys (as we do for the purposes of static analysis), we can recover a simpler version by setting f \triangleq id, which results in an updater h(x) = x[a \leftarrow g(x)], and a signature for the exercise:

{{< latex >}} \text{ex}{4.26} : \underbrace{K}{\text{value of}\ a} \to \underbrace{(\text{Map} \to V)}{\text{updater}\ g} \to \underbrace{\text{Map}}{\text{old map}} \to \underbrace{\text{Map}}_{\text{updated map}} {{< /latex >}}

This looks just like Haskell's Data.Map.adjust function, except that it can take the entire map into consideration when updating a key.

My generalized version takes in a list of keys to update, and makes the updater accept a key so that its behavior can be specialized for each entry it changes. The sketch of the implementation is in the _updating_via_ function from the Map module, and its helper transform. Here, I collapse its definition, since it's not particularly important.

{{< codelines "agda" "agda-spa/Lattice/Map.agda" 926 931 "" "(Click here to see the definition of transform)" >}}

The proof of monotonicity --- which is the solution to the exercise --- is actually quite complicated. I will omit its description, and show it here in another collapsed block.

{{< codelines "agda" "agda-spa/Lattice/Map.agda" 1042 1105 "" "(Click here to see the proof of monotonicity of h)" >}}

Given a proof of the exercise, all that's left is to instantiate the theorem with the argument I described. Specifically:

  • L_1 \triangleq \text{Info} \triangleq \text{NodeId} \to (\text{Variable} \to \text{Sign})
  • L_2 \triangleq \text{Variable} \to \text{Sign}
  • A \triangleq \text{NodeId}
  • f \triangleq \text{id} \triangleq x \mapsto x
  • g(k, m) = \text{JOIN}(k, m)

In the equation for g, I explicitly insert the map m instead of leaving it implicit as the textbook does. In Agda, this instantiation for joining all predecessor looks like this (using states as the list of keys to update, indicating that we should update every key):

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 152 157 >}}

And the one for evaluating all programs looks like this:

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 215 220 >}}

Actually, we haven't yet seen that updateVariablesFromStmt. This is a function that we can define using the user-provided abtract interpretation eval. Specifically, it handles the job of updating the sign of a variable once it has been assigned to (or doing nothing if the statement is a no-op). {#define-updateVariablesFromStmt}

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 191 193 >}}

The updateVariablesFromExpression is now new, and it is yet another map update, which changes the sign of a variable k to be the one we get from running eval on it. Map updates are instances of the generalized update; this time, the updater g is eval. The exercise requires the updater to be monotonic, which constrains the user-provided evaluation function to be monotonic too.

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 173 181 >}}

We finally write the analyze function as the composition of the two bulk updates:

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 226 232 >}}

Instantiating with the Sign Lattice

Thus far, I've been talking about the sign lattice throughout, but implementing the Agda code in terms of a general lattice L and evaluation function eval. In order to actually run the Agda code, we do need to provide an eval function, which implements the logic we used above, in which a zero-sign variable (x) minus one was determined to be negative. For binary operators specifically, I've used the table provided in the textbook; here they are:

{{< figure src="plusminus.png" caption="Cayley tables for abstract interpretation of plus and minus" >}}

These are pretty much common sense:

  • A positive plus a positive is still positive, so +\ \hat{+}\ + = +
  • A positive plus any sign could be any sign still, so +\ \hat{+}\ \top = \top
  • Any sign plus "impossible" is impossible, so \top\ \hat{+} \bot = \bot.
  • etc.

The Agda encoding for the plus function is as follows, and the one for minus is similar.

{{< codelines "agda" "agda-spa/Analysis/Sign.agda" 76 94 >}}

As the comment in the block says, it would be incredibly tedious to verify the monotonicity of these tables, since you would have to consider roughly 125 cases per argument: for each (fixed) sign s and two other signs s_1 \le s_2, we'd need to show that s\ \hat{+}\ s_1 \le s\ \hat{+}\ s_2. I therefore commit the faux pas of using postulate. Fortunately, the proof of monotonicity is not used for the execution of the program, so we will get away with this, barring any meddling kids.

From this, all that's left is to show that for any expression e, the evaluation function:

{{< latex >}} \text{eval} : \text{Expr} \to (\text{Variable} \to \text{Sign}) \to \text{Sign} {{< /latex >}}

is monotonic. It's defined straightforwardly and very much like an evaluator / interpreter, suggesting that "abstract interpretation" is the correct term here.

{{< codelines "agda" "agda-spa/Analysis/Sign.agda" 176 184 >}}

Thought it won't happen, it was easier to just handle the case where there's an undefined variable; I give it "any sign". Otherwise, the function simply consults the sign tables for + or -, as well as the known signs of the variables. For natural number literals, it assigns 0 the "zero" sign, and any other natural number the "(+)".

To prove monotonicity, we need to consider two variable maps (one less than the other), and show that the abstract interpretation respects that ordering. This boils down to the fact that the plus and minus tables are monotonic in both arguments (thus, if their sub-expressions are evaluated monotonically given an environment, then so is the whole addition or subtraction), and to the fact that for two maps m₁ ≼ m₂, the values at corresponding keys are similarly ordered: m₁[k] ≼ m₂[k]. We saw that above.

{{< codelines "agda" "agda-spa/Analysis/Sign.agda" 186 223 "" "(Click to expand the proof that the evaluation function for signs is monotonic)" >}}

That's all we need. With this, I just instantiate the Forward module we have been working with, and make use of the result. I also used a show function (which I defined) to stringify that output.

{{< codelines "agda" "agda-spa/Analysis/Sign.agda" 225 229 >}}

But wait, result? We haven't seen a result just yet. That's the last piece, and it involves finally making use of the fixed-point algorithm.

Invoking the Fixed Point Algorithm

Our \text{Info} lattice is of finite height, and the function we have defined is monotonic (by virtue of being constructed only from map updates, which are monotonic by Exercise 4.26, and from function composition, which preserves monotonicity). We can therefore apply the fixed-point-algorithm, and compute the least fixed point:

{{< codelines "agda" "agda-spa/Analysis/Forward.agda" 235 238 >}}

With this, analyze is the result of our forward analysis!

In a Main.agda file, I invoked this analysis on a sample program:

testCode : Stmt
testCode =
     "zero"  (# 0)  then
     "pos"  ((` "zero") Expr.+ (# 1))  then
     "neg"  ((` "zero") Expr.- (# 1))  then
     "unknown"  ((` "pos") Expr.+ (` "neg")) 

testProgram : Program
testProgram = record
    { rootStmt = testCode
    }

open WithProg testProgram using (output; analyze-correct)

main = run {0} (putStrLn output)

The result is verbose, since it shows variable signs for each statement in the program. However, the key is the last basic block, which shows the variables at the end of the program. It reads:

{"neg" ↦ -, "pos" ↦ +, "unknown" ↦ , "zero" ↦ 0, }

Verifying the Analysis

We now have a general framework for running forward analyses: you provide an abstract interpretation function for expressions, as well as a proof that this function is monotonic, and you get an Agda function that takes a program and tells you the variable states at every point. If your abstract interpretation function is for determining the signs of expressions, the final result is an analysis that determines all possible signs for all variables, anywhere in the code. It's pretty easy to instantiate this framework with another type of forward analysis --- in fact, by switching the plus function to one that uses AboveBelow , rather than AboveBelow Sign:

plus : ConstLattice  ConstLattice  ConstLattice
plus ⊥ᶜ _ = ⊥ᶜ
plus _ ⊥ᶜ = ⊥ᶜ
plus ⊤ᶜ _ = ⊤ᶜ
plus _ ⊤ᶜ = ⊤ᶜ
plus [ z₁ ]ᶜ [ z₂ ]ᶜ = [ z₁ Int.+ z₂ ]ᶜ

we can defined a constant-propagation analysis.

{"neg" ↦ -1, "pos" ↦ 1, "unknown" ↦ 0, "zero" ↦ 0, }

However, we haven't proved our analysis correct, and we haven't yet made use of the CFG-semantics equivalence that we [proved in the previous section]({{< relref "07_spa_agda_semantics_and_cfg" >}}). I was hoping to get to it in this post, but there was just too much to cover. So, I will get to that in the next post, where we will make use of the remaining machinery to demonstrate that the output of our analyzer matches reality.