blog-static/content/blog/03_spa_agda_fixed_height.md

556 lines
26 KiB
Markdown
Raw Normal View History

---
title: "Implementing and Verifying \"Static Program Analysis\" in Agda, Part 3: Lattices of Finite Height"
series: "Static Program Analysis in Agda"
description: "In this post, I describe the class of finite-height lattices, and prove that lattices we've alread seen are in that class"
date: 2024-08-08T17:29:00-07:00
tags: ["Agda", "Programming Languages"]
---
In the previous post, I introduced the class of finite-height lattices:
lattices where chains made from elements and the less-than operator `<`
can only be so long. As a first example,
[natural numbers form a lattice]({{< relref "01_spa_agda_lattices#natural-numbers" >}}),
but they __are not a finite-height lattice__; the following chain can be made
infinitely long:
{{< latex >}}
0 < 1 < 2 < ...
{{< /latex >}}
There isn't a "biggest natural number"! On the other hand, we've seen that our
[sign lattice]({{< relref "01_spa_agda_lattices#sign-lattice" >}}) has a finite
height; the longest chain we can make is three elements long; I showed one
such chain (there are many chains of three elements) in
[the previous post]({{< relref "02_spa_agda_combining_lattices#sign-three-elements" >}}),
but here it is again:
{{< latex >}}
\bot < + < \top
{{< /latex >}}
It's also true that the [Cartesian product lattice \(L_1 \times L_2\)]({{< relref "02_spa_agda_combining_lattices#the-cartesian-product-lattice" >}})
has a finite height, as long as \(L_1\) and \(L_2\) are themselves finite-height
lattices. In the specific case where both \(L_1\) and \(L_2\) are the sign
lattice (\(L_1 = L_2 = \text{Sign} \)) we can observe that the longest
chains have five elements. The following is one example:
{{< latex >}}
(\bot, \bot) < (\bot, +) < (\bot, \top) < (+, \top) < (\top, \top)
{{< /latex >}}
{#sign-prod-chain}
The fact that \(L_1\) and \(L_2\) are themselves finite-height lattices is
important; if either one of them is not, we can easily construct an infinite
chain of the products. If we allowed \(L_2\) to be natural numbers, we'd
end up with infinite chains like this one:
{#product-both-finite-height}
{{< latex >}}
(\bot, 0) < (\bot, 1) < (\bot, 2) < ...
{{< /latex >}}
Another lattice that has a finite height under certain conditions is
[the map lattice]({{< relref "02_spa_agda_combining_lattices#the-map-lattice" >}}).
The "under certain conditions" part is important; we can easily construct
an infinite chain of map lattice elements in general:
{{< latex >}}
\{ a : 1 \} < \{ a : 1, b : 1 \} < \{ a: 1, b: 1, c: 1 \} < ...
{{< /latex >}}
As long as we have infinite keys to choose from, we can always keep
adding new keys to make bigger and bigger maps. But if we fix the keys in
the map --- say, we use only `a` and `b` --- then suddenly our heights are once
again fixed. In fact, for the two keys I just picked, one longest chain
is remarkably similar to the product chain above.
{#fin-keys}
{{< latex >}}
\{a: \bot, a: \bot\} < \{a: \bot, b: +\} < \{a: \bot, b: \top\} < \{a: +, b: \top\} < \{a: \top, b: \top\}
{{< /latex >}}
The class of finite-height lattices is important for static program analysis,
because it ensures that out our analyses don't take infinite time. Though
there's an intuitive connection ("finite lattices mean finite execution"),
the details of why the former is needed for the latter are nuanced. We'll
talk about them in a subsequent post.
In the meantime, let's dig deeper into the notion of finite height, and
the Agda proofs of the properties I've introduced thus far.
### Formalizing Finite Height
The formalization I settled on is quite similar to the informal description:
a lattice has a finite height of length \(h\) if the longest chain
of elements compared by \((<)\) is exactly \(h\). There's only a slight
complication: we allow for equivalent-but-not-equal elements in lattices.
For instance, for a map lattice, we don't care about the order of the keys:
so long as two maps relate the same set of keys to the same respective values,
we will consider them equal. This, however, is beyond the notion of Agda's
propositional equality (`_≡_`). Thus, we we need to generalize the definition
of a chain to support equivalences. I parameterize the `Chain` module
in my code by an equivalence relation, as well as the comparison relation `R`,
which we will set to `<` for our chains. The equivalence relation `_≈_` and the
ordering relation `R`/`<` are expected to play together nicely (if `a < b`, and
`a` is equivalent to `c`, then it should be the case that `c < b`).
{{< codelines "agda" "agda-spa/Chain.agda" 3 7 >}}
From there, the definition of the `Chain` data type is much like the definition
of [a vector from `Data.Vec`](https://agda.github.io/agda-stdlib/v2.0/Data.Vec.Base.html#1111),
but indexed by the endpoints, and containing witnesses of `R`/`<`
between its elements. The indexing allows for representing
the type of chains between particular lattice elements, and serves to ensure
concatenation and other operations don't merge disparate chains.
{{< codelines "agda" "agda-spa/Chain.agda" 19 21 >}}
In the `done` case, we create a single-element chain, which has no comparisons.
In this case, the chain starts and stops at the same element (where "the same"
is modulo our equivalence). The `step` case prepends a new comparison `a1 < a2`
to an existing chain; once again, we allow for the existing chain to start
with a different-but-equivalent element `a2'`.
With that definition in hand, I define what it means for a type of
chains between elements of the lattice `A` to be bounded by a certain height; simply
put, all chains must have length less than or equal to the bound.
{{< codelines "agda" "agda-spa/Chain.agda" 38 39 >}}
Though `Bounded` specifies _a_ bound on the length of chains, it doesn't
specify _the_ (lowest) bound. Specifically, if the chains can only have
length three, they are bounded by both 3, 30, and 300. To claim a lowest
bound (which would be the maximum length of the lattice), we need to show that
a chain of that length actually exists (otherwise,
we could take the previous natural number, and it would be a bound as well).
Thus, I define the `Height` predicate to require that a chain of the desired
height exists, and that this height bounds the length of all other chains.
{{< codelines "agda" "agda-spa/Chain.agda" 47 53 >}}
Finally, for a lattice to have a finite height, the type of chains formed by using
its less-than operator needs to have that height (satisfy the `Height h` predicate).
To avoid having to thread through the equivalence relation, congruence proof,
and more, I define a specialized predicate for lattices specifically.
I do so as a "method" in my `IsLattice` record.
{{< codelines "agda" "agda-spa/Lattice.agda" 183 210 "hl_lines = 27 28">}}
Thus, bringing the operators and other definitions of `IsLattice` into scope
will also bring in the `FixedHeight` predicate.
### Fixed Height of the "Above-Below" Lattice
We've already seen intuitive evidence that the sign lattice --- which is an instance of
the ["above-below" lattice]({{< relref "01_spa_agda_lattices#the-above-below-lattice" >}}) ---
has a fixed height. The reason is simple: we extended a set of incomparable
elements with a single element that's greater, and a single element that's lower.
We can't make chains out of incomparable elements (since we can't compare them
using `<`); thus, we can only have one `<` from the new least element, and
one `<` from the new greatest element.
The proof is a bit tedious, but not all that complicated.
First, a few auxiliary helpers; feel free to read only the type signatures.
They specify, respectively:
1. That the bottom element \(\bot\) of the above-below lattice is less than any
concrete value from the underlying set. For instance, in the sign lattice case, \(\bot < +\).
2. That \(\bot\) is the only element satisfying the first property; that is,
any value strictly less than an element of the underlying set must be \(\bot\).
3. That the top element \(\top\) of the above-below lattice is greater than
any concrete value of the underlying set. This is the dual of the first property.
4. That, much like the bottom element is the only value strictly less than elements
of the underlying set, the top element is the only value strictly greater.
{{< codelines "agda" "agda-spa/Lattice/AboveBelow.agda" 315 335 >}}
From there, we can construct an instance of the longest chain. Actually,
there's a bit of a hang-up: what if the underlying set is empty? Concretely,
what if there were no signs? Then we could only construct a chain with
one comparison: \(\bot < \top\). Instead of adding logic to conditionally
specify the length, I simply require that the set is populated by requiring
a witness
{{< codelines "agda" "agda-spa/Lattice/AboveBelow.agda" 85 85 >}}
I use this witness to construct the two-`<` chain.
{{< codelines "agda" "agda-spa/Lattice/AboveBelow.agda" 339 340 >}}
The proof that the length of two --- in terms of comparisons --- is the
bound of all chains of `AboveBelow` elements requires systematically
rejecting all longer chains. Informally, suppose you have a chain of
three or more comparisons.
1. If it starts with \(\top\), you can't add any more elements since that's the
greatest element (contradiction).
2. If you start with an element of the underlying set, you could add another
element, but it has to be the top element; after that, you can't add any
more (contradiction).
3. If you start with \(\bot\), you could arrive at a chain of two comparisons,
but you can't go beyond that (in three cases, each leading to contradictions).
{{< codelines "agda" "agda-spa/Lattice/AboveBelow.agda" 342 355 "hl_lines=8-14">}}
Thus, the above-below lattice has a length of two comparisons (or alternatively,
three elements).
{{< codelines "agda" "agda-spa/Lattice/AboveBelow.agda" 357 363 >}}
And that's it.
### Fixed Height of the Product Lattice
Now, for something less tedious. We saw above that for a product lattice
to have a finite height,
[its constituent lattices must have a finite height](#product-both-finite-height).
The proof was by contradiction (by constructing an infinitely long product
chain given a single infinite lattice). As a result, we'll focus this
section on products of two finite lattices `A` and `B`. Additionally, for the
proofs in this section, I require element equivalence to be decidable.
{{< codelines "agda" "agda-spa/Lattice/Prod.agda" 115 117 >}}
Let's think about how we might go about constructing the longest chain in
a product lattice. Let's start with some arbitrary element \(p_1 = (a_1, b_1)\).
We need to find another value that isn't equal to \(p_1\), because we're building
chains of the less-than operator \((<)\), and not the less-than-or-equal operator
\((\leq)\). As a result, we need to change either the first component, the second
component, or both. If we're building "to the right" (adding bigger elements),
the new components would need to be bigger. Suppose then that we came up
with \(a_2\) and \(b_2\), with \(a_1 < a_2\) and \(b_1 < b_2\). We could then
create a length-one chain:
{{< latex >}}
(a_1, b_1) < (a_2, b_2)
{{< /latex >}}
That works, but we can construct an even longer chain by increasing only one
element at a time:
{{< latex >}}
(a_1, b_1) < (a_1, b_2) < (a_2, b_2)
{{< /latex >}}
We can apply this logic every time; the conclusion is that when building
up a chain, we need to increase one element at a time. Then, how many times
can we increase an element? Well, if lattice `A` has a height of two (comparisons),
then we can take its lowest element, and increase it twice. Similarly, if
lattice `B` has a height of three, starting at its lowest element, we can
increase it three times. In all, when building a chain of `A × B`, we can
increase an element five times. Generally, the number of `<` in the product chain
is the sum of the numbers of `<` in the chains of `A` and `B`.
This gives us a recipe for constructing
the longest chain in the product lattice: take the longest chains of `A` and
`B`, and start with the product of their lowest elements. Then, increase
the elements one at a time according to the chains. The simplest way to do
that might be to increase by all elements of the `A` chain, and then
by all of the elements of the `B` chain (or the other way around). That's the
strategy I took when [constructing the \(\text{Sign} \times \text{Sign}\)
chain above](#sign-prod-chain).
To formalize this notion, a few lemmas. First, given two chains where
one starts with the same element another ends with, we can combine them into
one long chain.
{{< codelines "agda" "agda-spa/Chain.agda" 31 33 >}}
More interestingly, given a chain of comparisons in one lattice, we are
able to lift it into a chain in another lattice by applying a function
to each element. This function must be monotonic, because it must not
be able to reverse \(a < b\) such that \(f(b) < f(a)\). Moreover, this function
should be injective, because if \(f(a) = f(b)\), then a chain \(a < b\) might
be collapsed into \(f(a) \not< f(a)\), changing its length. Finally,
the function needs to produce equivalent outputs when giving equivalent inputs.
The result is the following lemma:
{{< codelines "agda" "agda-spa/Lattice.agda" 226 247 >}}
Given this, and two lattices of finite height, we construct the full product
chain by lifting the `A` chain into the product via \(a \mapsto (a, \bot_2)\),
lifting the `B` chain into the product via \(b \mapsto (\top_1, b)\), and
concatenating the results. This works because the first chain ends with
\((\top_1, \bot_2)\), and the second starts with it.
{{< codelines "agda" "agda-spa/Lattice/Prod.agda" 169 171 >}}
This gets us the longest chain; what remains is to prove that this chain's
length is the bound of all other changes. To do so, we need to work in
the opposite direction; given a chain in the product lattice, we need to
somehow reduce it to chains in lattices `A` and `B`, and leverage their
finite height to complete the proof.
The key idea is that for every two consecutive elements in the product lattice
chain, we know that at least one of their components must've increased.
This increase had to come either from elements in lattice `A` or in lattice `B`.
We can thus stick this increase into an `A`-chain or a `B`-chain, increasing
its length. Since one of the chains grows with every consecutive pair, the
number of consecutive pairs can't exceed the combined lengths of the `A` and `B` chains.
I implement this idea as an `unzip` function, which takes a product chain
and produces two chains made from its increases. By the logic we've described,
the length two chains has to bound the main one's. I give the signature below,
and will put the implementation in a collapsible detail block. One last
detail is that the need to decide which chain to grow --- and thus which element
has increased --- is what introduces the need for decidable equality.
{{< codelines "agda" "agda-spa/Lattice/Prod.agda" 149 149 >}}
{{< codelines "agda" "agda-spa/Lattice/Prod.agda" 149 163 "" "**(Click here for the implementation of `unzip`)**" >}}
Having decomposed the product chain into constituent chains, we simply combine
the facts that they have to be bounded by the height of the `A` and `B` lattices,
as well as the fact that they bound the combined chain.
{{< codelines "agda" "agda-spa/Lattice/Prod.agda" 165 175 "hl_lines = 8-10" >}}
This completes the proof!
### Iterated Products
The product lattice allows us to combine finite height lattices into a
new finite height lattice. From there, we can use this newly created lattice
as a component of yet another product lattice. For instance, if we had
\(L_1 \times L_2\), we can take a product of that with \(L_1\) again,
and get \(L_1 \times (L_1 \times L_2)\). Since this also creates a
finite-height lattice, we can repeat this process, and keep
taking a product with \(L_1\), creating:
{{< latex >}}
\overbrace{L_1 \times ... \times L_1}^{n\ \text{times}} \times L_2.
{{< /latex >}}
I call this the _iterated product lattice_. Its significance will become
clear shortly; in the meantime, let's prove that it is indeed a lattice
(of finite height).
To create an iterated product lattice, we still need two constituent
lattices as input.
{{< codelines "agda" "agda-spa/Lattice/IterProd.agda" 7 11 >}}
{{< codelines "agda" "agda-spa/Lattice/IterProd.agda" 23 24 >}}
At a high level, the proof goes by induction on the number of applications
of the product. There's just one trick. I'd like to build up an `isLattice`
instance even if `A` and `B` are not finite-height. That's because in
that case, the iterated product is still a lattice, just not one with
a finite height. On the other hand, the `isFiniteHeightLattice` proof
requires the `isLattice` proof. Since we're building up by induction,
that means that every recursive invocation of the function, we need
to get the "partial" lattice instance and give it to the "partial" finite
height lattice instance. When I implemented the inductive proof for
`isLattice` independently from the (more specific) inductive proof
of `isFiniteHeightLattice`, Agda could not unify the two `isLattice`
instances (the "actual" one and the one that serves as witness
for `isFiniteHeightLattice`). This led to some trouble and inconvenience,
and so, I thought it best to build the two up together.
To build up with the lattice instance and --- if possible --- the finite height
instance, I needed to allow for the constituent lattices being either finite
or infinite. I supported this by defining a helper type:
{{< codelines "agda" "agda-spa/Lattice/IterProd.agda" 40 55 >}}
Then, I defined the "everything at once" type, in which, instead of
a field for the proof of finite height, has a field that constructs
this proof _if the necessary additional information is present_.
{{< codelines "agda" "agda-spa/Lattice/IterProd.agda" 57 76 >}}
Finally, the proof by induction. It's actually relatively long, so I'll
include it as a collapsible block.
{{< codelines "agda" "agda-spa/Lattice/IterProd.agda" 78 120 "" "**(Click here to expand the inductive proof)**" >}}
### Fixed Height of the Map Lattice
We saw above that [we can make a map lattice have a finite height if
we fix its keys](#finite-keys). How does this work? Well, if the keys
are always the same, we can think of such a map as just a tuple, with
as many element as there are keys.
{{< latex >}}
\begin{array}{cccccc}
\{ & a: 1, & b: 2, & c: 3, & \} \\
& & \iff & & \\
( & 1, & 2, & 3 & )
\end{array}
{{< /latex >}}
This is why I introduced [iterated products](#iterated-products) earlier;
we can use them to construct the second lattice in the example above.
I'll take one departure from that example, though: I'll "pad" the tuples
with an additional unit element at the end. The unit type (denoted \(\top\))
--- which has only a single element --- forms a finite height lattice trivially;
I prove this in [an appendix below](#appendix-the-unit-lattice).
Using this padding helps reduce the number of special cases; without the
adding, the tuple definition might be something like the following:
{{< latex >}}
\text{tup}(A, k) =
\begin{cases}
\top & k = 0 \\
A & k = 1 \\
A \times \text{tup}(A, k - 1) & k > 1
\end{cases}
{{< /latex >}}
On the other hand, if we were to allow the extra padding, we could drop
the definition down to:
{{< latex >}}
\text{tup}(A, k) = \text{iterate}(t \mapsto A \times t, k, \bot) =
\begin{cases}
\top & k = 0 \\
A \times \text{tup}(A, k - 1) & k > 0
\end{cases}
{{< /latex >}}
And so, we drop from two to three cases, which means less proof work for us.
The tough part is to prove that the two representations of maps --- the
key-value list and the iterated product --- are equivalent. We will not
have much trouble proving that they're both lattices (we did that last time,
for both [products]({{< relref "02_spa_agda_combining_lattices#the-cartesian-product-lattice" >}}) and [maps]({{< relref "02_spa_agda_combining_lattices#the-map-lattice" >}})). Instead, what we need to do is prove that
the height of one lattice is the same as the height of the other. We prove
this by providing something like an [isomorphism](https://mathworld.wolfram.com/Isomorphism.html):
a pair of functions that convert between the two representations, and
preserve the properties and relationships (such as \((\sqcup)\)) of lattice
elements. In fact, the list of the conversion functions' properties is quite
extensive:
{{< codelines "agda" "agda-spa/Isomorphism.agda" 22 33 "hl_lines=8-12">}}
1. First, the functions must preserve our definition of equivalence. Thus,
if we convert two equivalent elements from the list representation to
the tuple representation, the resulting tuples should be equivalent as well.
The reverse must be true, too.
2. Second, the functions must preserve the binary operations --- see also the definition
of a [homomorphism](https://en.wikipedia.org/wiki/Homomorphism#Definition).
Specifically, if \(f\) is a conversion function, then the following
should hold:
{{< latex >}}
f(a \sqcup b) \approx f(a) \sqcup f(b)
{{< /latex >}}
For the purposes of proving that equivalent maps have finite heights, it
turns out that this property need only hold for the join operator \((\sqcup)\).
3. Finally, the functions must be inverses of each other. If you convert a
list to a tuple, and then the tuple back into a list, the resulting
value should be equivalent to what we started with. In fact, they
need to be both "left" and "right" inverses, so that both \(f(g(x))\approx x\)
and \(g(f(x)) \approx x\).
Given this, the high-level proof is in two parts:
1. __Proving that a chain of the same height exists in the second (e.g., tuple)
lattice:__ To do this, we want to take the longest chain in the first
(e.g. key-value list) lattice, and convert it into a chain in the second.
The mechanism for this is not too hard to imagine: we just take the original
chain, and apply the conversion function to each element.
Intuitively, this works because of the structure-preserving properties
we required above. For instance (recall the
[definition of \((\leq)\) given by Lars Hupel](https://lars.hupel.info/topics/crdt/03-lattices/#there-), which in brief is \(a \leq b \triangleq a \sqcup b = b\)):
{{< latex >}}
\begin{array}{rcr}
a \leq b & \iff & (\text{definition of less than})\\
a \sqcup b \approx b & \implies & (\text{conversions preserve equivalence}) \\
f(a \sqcup b) \approx f(b) & \implies & (\text{conversions distribute over binary operations}) \\
f(a) \sqcup f(b) \approx f(b) & \iff & (\text{definition of less than}) \\
f(a) \leq f(b)
\end{array}
{{< /latex >}}
2. __Proving that longer chains can't exist in the second (e.g., tuple) lattice:__
we've already seen the mechanism to port a chain from one lattice to
another lattice, and we can use this same mechanism (but switching directions)
to go in reverse. If we do that, we can take a chain of questionable length
in the tuple lattice, port it back to the key-value map, and use the
(already known) fact that its chains are bounded to conclude the same
thing about the tuple chain.
As you can tell, the chain porting mechanism is doing the heavy lifting here.
It's relatively easy to implement given the conditions we've set on
conversion functions, in both directions:
{{< codelines "agda" "agda-spa/Isomorphism.agda" 52 64 >}}
With that, we can prove the second lattice's finite height:
{{< codelines "agda" "agda-spa/Isomorphism.agda" 66 80 >}}
The conversion functions are also not too difficult to define. I give
them below, but I refrain from showing proofs of the more involved
properties (such as the fact that `from` and `to` are inverses, preserve
equivalence, and distribute over join) here. You can view them by clicking
the link at the top of the code block below.
{{< codelines "agda" "agda-spa/Lattice/FiniteValueMap.agda" 68 85 >}}
Above, `FiniteValueMap ks` is the type of maps whose keys are fixed to
`ks`; defined as follows:
{{< codelines "agda" "agda-spa/Lattice/FiniteMap.agda" 58 60 >}}
Proving the remaining properties (which as I mentioned, I omit from
the main body of the post) is sufficient to apply the isomorphism,
proving that maps with finite keys are of a finite height.
### Using the Finite Height Property
Lattices having a finite height is a crucial property for the sorts of
static program analyses I've been working to implement.
We can create functions that traverse "up" through the lattice,
creating larger values each time. If these lattices are of a finite height,
then the static analyses functions can only traverse "so high".
Under certain conditions, this
guarantees that our static analysis will eventually terminate with
a [fixed point](https://mathworld.wolfram.com/FixedPoint.html). Pragmatically,
this is a state in which running our analysis does not yield any more information.
The way that the fixed point is found is called the _fixed point algorithm_.
We'll talk more about this in the next post.
{{< seriesnav >}}
### Appendix: The Unit Lattice
The unit lattice is a relatively boring one. I use the built-in unit type
in Agda, which (perhaps a bit confusingly) is represented using the symbol ``.
It only has a single constructor, `tt`.
{{< codelines "agda" "agda-spa/Lattice/Unit.agda" 6 7 >}}
The equivalence for the unit type is just propositional equality (we have
no need to identify unequal values of ``, since there is only one value).
{{< codelines "agda" "agda-spa/Lattice/Unit.agda" 17 25 >}}
Both the join \((\sqcup)\) and meet \((\sqcap)\) operations are trivially defined;
in both cases, they simply take two `tt`s and produce a new `tt`.
Mathematically, one might write this as \((\text{tt}, \text{tt}) \mapsto \text{tt}\).
In Agda:
{{< codelines "agda" "agda-spa/Lattice/Unit.agda" 30 34 >}}
These operations are trivially associative, commutative, and idempotent.
{{< codelines "agda" "agda-spa/Lattice/Unit.agda" 39 46 >}}
That's sufficient for them to be semilattices:
{{< codelines "agda" "agda-spa/Lattice/Unit.agda" 48 54 >}}
The [absorption laws]({{< relref "01_spa_agda_lattices#absorption-laws" >}})
are also trivially satisfied, which means that the unit type forms a lattice.
{{< codelines "agda" "agda-spa/Lattice/Unit.agda" 78 90 >}}
Since there's only one element, it's not really possible to have chains
that contain any more than one value. As a result, the height (in comparisons)
of the unit lattice is zero.
{{< codelines "agda" "agda-spa/Lattice/Unit.agda" 102 117 >}}