Lattices and Fixed Points
Every dataflow analysis you meet — reaching definitions, available expressions, live variables, constant
propagation — looks like a different puzzle with its own gen/kill sets and its own initial values. But
they are all secretly the same algorithm, and that algorithm rests on one piece of order theory:
the lattice. Casting an analysis as "iterate a monotone function over a lattice until it
stops changing" is what lets us answer, once and for all and for every analysis at once, three
questions a working compiler writer cannot dodge: Does the iteration terminate? Is the answer it
lands on the right one? And how precise is it compared to the truth?
This is the theory Gary Kildall laid down in 1973 and Kam & Ullman sharpened. It sits directly
beneath the
dataflow framework:
the framework tells you how to set up gen/kill and meet; this page tells you why the
setup is guaranteed to work.
Partial orders and meet-semilattices
The dataflow "facts" at a program point form a set L with an ordering
\sqsubseteq that reads as "is at least as conservative as" (or "carries no
more information than"). It is a partial order — reflexive, antisymmetric, transitive
— partial because some facts are simply incomparable (neither
x \sqsubseteq y nor y \sqsubseteq x). What makes it
a meet-semilattice is that any two elements have a greatest lower bound, their
meet x \wedge y — the operation that combines the facts
arriving on different control-flow edges.
The workhorse example is the powerset lattice above: all subsets of a universe
(here \{a,b,c\}), ordered by \subseteq (or its
reverse), drawn as a Hasse diagram — an edge upward means "immediately above in the
order". Two special elements anchor it:
| Element | Name | Role |
| \top | top |
the greatest element (most optimistic / most information); the identity for meet: \top \wedge x = x |
| \bot | bottom |
the least element (most conservative); absorbing for meet: \bot \wedge x = \bot |
Meet is idempotent, commutative and associative, and it induces the order:
x \sqsubseteq y \iff x \wedge y = x. That single algebraic fact is the hinge
the whole termination argument will swing on.
Transfer functions: monotone, and sometimes distributive
Each basic block acts on facts through a transfer function
f_B : L \to L. Two properties of these functions decide everything:
\textbf{Monotone:}\quad x \sqsubseteq y \;\Longrightarrow\; f(x) \sqsubseteq f(y).
\textbf{Distributive:}\quad f(x \wedge y) \;=\; f(x) \wedge f(y).
Monotonicity says the function never inverts the order: feed it more
information and you never get out less. It is a mild, almost always satisfiable condition — every
gen/kill function f(x) = \mathit{gen} \cup (x \setminus \mathit{kill}) is
monotone — and it is exactly what we need for the iteration to converge sensibly.
Distributivity is strictly stronger: it says the function commutes with meet, so that
merging paths and then transforming gives the same answer as transforming and then merging. Every
bit-vector analysis (reaching defs, available expressions, liveness) is distributive;
constant propagation
is the famous analysis that is monotone but not distributive — and, as we will see, it pays a
precision price for that.
Why the iteration terminates
The iterative solver starts every point at \top and repeatedly applies
transfer functions and meets until nothing changes. Each such step can only move a value down
the lattice (or leave it), producing a descending chain
x_0 \sqsupseteq x_1 \sqsupseteq x_2 \sqsupseteq \cdots. If that chain can only
descend finitely far, the process must stop. Two equivalent guarantees ensure it:
- if every transfer function is monotone and the lattice has finite
height (the longest \sqsubseteq-chain has length
h < \infty), then the iteration terminates;
- more generally it suffices that the lattice satisfies the descending chain condition
(no infinite strictly-descending chain);
- at termination the solver has reached the Maximum Fixed Point (MFP) — the
greatest solution of the dataflow equations x = f(x) reachable from
\top.
The height even bounds the cost: a value at any single point can change at most
h times, so a round-robin solver over n points
converges in O(n \cdot h) value-updates. For a powerset lattice over a
universe of size k, h = k; for constant
propagation, h = 3 per variable. Finite height is not a technicality — it is
the entire reason a compiler can promise the optimiser will actually finish.
That the fixed point exists at all is Knaster–Tarski: a monotone function on a complete
lattice has a complete lattice of fixed points, with a greatest and a least one. Iterating from
\top converges to the greatest fixed point below your start — the MFP.
Iterating to a fixed point, in code
Concretely, "solve the analysis" means "find x with
x = f(x) by iterating from \top". Below, a tiny
powerset lattice over \{0,1,2,3\} (as a 4-bit set) and a monotone function
that removes bit 3, keeps bit 0, and adds bit 1. We iterate from \top
(all bits) and count the rounds until the value stops changing.
// Powerset lattice over {0,1,2,3} as a 4-bit set; TOP = all bits, meet = intersection.
const TOP = 0b1111;
// A monotone transfer function: keep only bits 0 and 1, drop bits 2 and 3.
// Masking is monotone -- feeding it a larger set never yields a larger result.
function f(x: number): number {
return x & 0b0011; // keep bits {0,1}
}
const show = (m: number) =>
"{" + [0, 1, 2, 3].filter((i) => m & (1 << i)).join(",") + "}";
let x = TOP, rounds = 0, prev = -1;
console.log(`start (⊤): ${show(x)}`);
while (x !== prev) {
prev = x;
x = f(x); // descend the lattice
rounds++;
console.log(`round ${rounds}: ${show(x)}`);
}
console.log(`fixed point reached after ${rounds} round(s): x = f(x) = ${show(x)}`);
The value only ever loses or holds bits — a descending chain — so with a lattice of height 4 it must
settle fast, and it does: after the first application it is already at the fixed point
{0,1}, confirmed by a second round that changes nothing. Swap in any other monotone
f and the same guarantee holds; write a non-monotone one (say, toggle a bit) and
you can make it oscillate forever — which is precisely why monotonicity is not optional.
MFP versus MOP — how good is the answer?
There is an "ideal" solution against which to judge the iterative one. The Meet-Over-all-Paths
(MOP) solution at a point p runs each path to p
separately, composing that path's transfer functions, and then meets the results over all paths:
\mathrm{MOP}(p) \;=\; \bigwedge_{\text{paths } P:\, \text{entry}\,\to\,p} f_P(\top), \qquad f_P = f_{B_k}\circ\cdots\circ f_{B_1}.
MOP is what you would ideally want — but it is generally uncomputable, because a loop
creates infinitely many paths. The iterative MFP solution is the computable stand-in, and the
relationship between them is the central theorem of dataflow analysis (Kam & Ullman, 1977):
\mathrm{MFP} \;\sqsubseteq\; \mathrm{MOP}, \qquad\text{with equality} \iff \text{every } f_B \text{ is distributive.}
In words: the iterative solution is always a safe approximation — at or below MOP in
the lattice, i.e. never more optimistic than the truth — and it is exactly MOP when the
framework is distributive. Because the MFP meets values early (at each merge, before applying
the next transfer), while MOP meets late (only at the very end), a non-distributive
f can lose precision: constant propagation's
a{+}b-after-a-merge example is the canonical gap, where MFP reports NAC and
MOP would have found a constant. The bit-vector analyses close the gap entirely, which is why they are so
beloved: for them, the cheap computable answer is the ideal one.
Starting from \top (the optimistic "assume the best") and descending gives you
the greatest fixed point — the most precise safe answer the equations admit. Start from
\bot and ascend and you converge to the least fixed point, which is
typically far too conservative to be useful (everything is "not a constant", nothing is "available").
Knaster–Tarski guarantees both exist; the analysis picks the top-down direction precisely because it wants
maximum precision consistent with safety. The optimism is safe only because it is paired with a
monotone f and a finite descent — the value can be walked back down as
contradicting paths are discovered, but can never be walked back up into unsoundness.
Two things learners over-claim. First, that the iterative answer is the answer: it is not
necessarily the ideal MOP answer. When the framework is not distributive — constant
propagation being the standard example — you can have
\mathrm{MFP} \sqsubset \mathrm{MOP} strictly: the solver is
safe but conservative, reporting less than the meet-over-all-paths truth. Monotonicity
alone buys you a safe fixed point, not an optimal one; distributivity is the extra ingredient
that makes MFP equal MOP. Second, termination is not automatic: it needs the
lattice to have finite height, or at least to satisfy the descending chain
condition (no infinitely long strictly-descending chain). A lattice of infinite height with a
monotone function can iterate forever — which is exactly why real analyses either use a finite-height
lattice (bit vectors) or bolt on a widening operator to force finite descent.