Jaideep M. More

software engineer


Program Analysis

  1. Data Flow Analysis
    1. Dead Code Analysis
    2. Definite Assignment
  2. Lattices and Fix point of monotone function
    1. Algorithms
  3. Constant Propagation
    1. Unreachable Code
    2. Domain Cooperation (Unreachable code and Constant Propagation)
  4. Abstract Interpretation
  5. Pentagons and Bound Checking
  6. Global Value Numbering

Dead Code Analysis:

Goal: To eliminate the computation whose result is unused

Definition: A variable xx is live at a program point in the CFG if there is a path from this program point to another program point where this variable is used which is not a definition.

Soundness: We must not eliminate computations whose result are live. Every over approximation is sound while eliminating dead code,

How do we find the set of live variables?

To determine the set of live variable at a program point pp. For every variable vv, we look at all the paths from point pp as the source. If we find any path where the variable is used v1←vv_{1} \leftarrow v . Then the variable vv is live at that program point pp.

A Local Approach:

For an edge (u,v)(u, v) in the CFG, assume we know the set of live variables at program point vv. Then we can calculate the set of the live variables at uu using this information.

for every vertex vv that can be reached by uu

u⟶[y←e(x1,⋯xn)]⟶v u \longrightarrow [y \leftarrow e(x_{1}, \cdots x_{n})] \longrightarrow v Lu⊇(Lv−{y})∪{x1,x2,…xn} L_u \supseteq (L_v - \{y\}) \cup \{x_1, x_2, \dots x_n\} lv(uv)(L)=(L−defuv)∪useuv \text{lv}_{(uv)}(L) = (L - \text{def}_{uv}) \cup \text{use}_{uv}

The in-equation system:

Our in-equation system has an equation for each edge in the CFG.

For each node we uu can write the in-equation as the union of all the edges of the form (u,v)(u, v)

Lu⊇⋃v∈succ(u)lvuv(Lv) L_u \supseteq \bigcup_{v \in \text{succ}(u)} \text{lv}_{uv}(L_v)

We can use a function L\mathbb{L} that maps the set of program points VV to a set of variables that are live at that program point.

L:V→P(Vars) \mathbb{L}: V \rightarrow \mathscr{P}(\text{Vars})

A trivial example of L\mathbb{L} maps each program point to the complete set of variables (all the variables are live at all the program points).

LV:L→L \text{LV}: \mathbb{L} \rightarrow \mathbb{L}

LV\text{LV} is a transformer function. Takes in a particular mapping of program points to the set of live variables and transforms it into a new mapping.

NOTE: The best solution for the set of live variables L\mathbb{L} is obtained by the LFP of the LV\text{LV}

Definite Assignment:

Goal: Check that on each path to use a variable, the variable has been defined before the use.

Definition: A variable xx is definitely assigned at program point pp if every path from rr (root of the CFG) to pp contains a definition of xx

Soundness: Every non-definitely assigned variable must be reported. warning about a definitely assigned variable is tolerable.

Thus, every under approximation of definitely assigned variables is sound.

A Local Approach:

For an edge (u,v)(u, v) in the CFG, assume we know the set of definitely assigned variables at program point uu. Then we can calculate the set of the definitely assigned variables at vv using this information.

for every vertex vv that can be reached by uu

u⟶[y←e(x1,⋯xn)]⟶v u \longrightarrow [y \leftarrow e(x_{1}, \cdots x_{n})] \longrightarrow v Av⊆Au∪{y} A_v \subseteq A_u \cup \{y\} da(uv)(A)=A∪defuv \text{da}_{(uv)}(A) = A \cup \text{def}_{uv}

The in-equation system:

Our in-equation system has an equation for each edge in the CFG.

For each node we vv can write the in-equation as the intersection of all the edges of the form (u,v)(u, v)

Av⊆⋂u∈pred(v)dauv(Au) A_v \subseteq \bigcap_{u \in \text{pred}(v)} \text{da}_{uv}(A_{u})

We can use a function A\mathbb{A} that maps the set of program points VV to a set of variables that are live at that program point.

A:V→P(Vars) \mathbb{A}: V \rightarrow \mathscr{P}(\text{Vars})

A trivial example of A\mathbb{A} maps each program point to the empty set of variables (all the variables are not assigned at all the program points).

DA:A→A \text{DA}: \mathbb{A} \rightarrow \mathbb{A}

DA\text{DA} is a transformer function. Takes in a particular mapping of program points to the set of definitely assigned variables and transforms it into a new mapping.

Open Question:

So far we have the following:

  1. Analysis Domain,(For Liveness L:V→P(Vars)\mathbb{L}: V \rightarrow \mathscr{P}(\text{Vars})): Gives the info derived about the program
  2. Transfer function on the edges (For liveness lvuv\text{lv}_{uv})
  3. A join operator compatible with the order of the in-equation (⋃\bigcup for Liveness).
  4. Using both we can build the single big function (LV\text{LV})

How do we compute a solution for the in-equation systems?

Lattices and Fix-points of Monotone Functions:

  • Lattice: A POSET(Set of elements PP, ordering relation ≤\le) is called a Lattice. Iff for every pair of element {x,y}\{x, y\} there exists a ⋁{x,y}\bigvee \{x, y\} and ⋀{x,y}\bigwedge \{x, y\}

  • Complete Lattice: A Lattice is called a complete lattice iff for every S⊆PS \subseteq P . SS is a Lattice.

  • Conditions for Completeness: Let (P,≤)(P, \le) be an ordered set, then the following statements are equivalent.

    1. PP is a complete lattice
    2. for every S⊆PS \subseteq P there exists a ⋁\bigvee (By dual ⋀\bigwedge exists).
  • ACC - Ascending Chain Conditions: An ordered set PP fulfils the ascending chain condition, if for every sequence

    x1≤x2⋯xn≤xi∈P x_1 \le x_2 \cdots x_n \le x_i \in P

    then there exists k∈Nk \in \mathbb{N} such that xk=xk+1=⋯x_{k} = x_{k+1} = \cdots

  • Lifting: Let (P,≤)(P, \le) be an ordered set/complete lattice. Let VV be a set. (V→P,⊑)(V\rightarrow P, \sqsubseteq) is an ordered set/complete lattice with

    a⊑b:  ∀v∈V, a(v)≤b(v) a \sqsubseteq b : \ \ \forall v \in V, \ a(v) \le b(v)
  • Knaster-Tarski Fixpoint Theorem: Let LL be a complete lattice and f:L→Lf: L \rightarrow L be a monotone function. then

    ⋀{f(x)≤x ∣ x∈L} \bigwedge \{ f(x) \le x \ |\ x \in L \}

    is the least fix point.

lfp_latex

For the upper part of the diagram:

  1. is the reductive part f(x)≤xf(x) \le x
  2. Is a sound solution for the Liveness analysis
  3. lfp\text{lfp} is the best solution and contains the least number of variables live at each program point

For the lower part of the diagram:

  1. is the expansive part f(x)≥xf(x) \ge x
  2. Is a sound solution for the Definite Assignment
  3. gfp\text{gfp} is the best solution and contains the maximum number of variables defined at each program point.

Thus by Knaster-Tarski theorem we know about the existence of the best possible solution for the LVLV and DADA analysis.

The question is how do we reach them.

Fixpoint by Iteration (Variation of Kleene’s theorem):

Let LL be a complete lattice that satisfies ACC and f:L→Lf: L \rightarrow L a monotone function. then there exists a kk such that fk(⊥)f^k(\bot) is lfp\text{lfp} of ff

The BIG Picture:

  1. We require the analysis domain to be a complete lattice with ACC.

    for liveness and definite assignment, we had V→P(Vars)V → \mathscr{P}(Vars) which is a complete lattice as P(vars)\mathscr{P}(vars) is a complete lattice with ACC.

  2. We require every local edge transformer fuvf_{uv} to be a monotone function. Then the big Function FF (LV\text{LV} and DA\text{DA}) are monotone.

  3. WE compute the FF’s fixpoint by using the Kleene’s theorem.

Chaotic Iteration:

lfp F\text{lfp}\ F is the best most precise solution for our inequation system and thus the the most analysis.

We can reach this lfp\text{lfp} by. updating only one program point per step as long as we make progress

Constant Propagation:

Goal: Eliminate computations on the variables that are constant

Domain: V→(Vars→Z⊥⊤)V \rightarrow (Vars \rightarrow Z_{\bot}^{\top}) The domain of the analysis defines for each program point a information for each variable. The information either specifies a constant value or TOP(Not constant) or BOTTOM(not assigned)

Over approximates concrete states of program execution at program point.

⊤\top - there exists two states with different values for variable.

⊥\bot - the variable is not assigned for every state.

If there exists one state where the variable is assigned and is not assigned in all other states then, the un assigned state leads to undefined behaviour and are thus ignored. The analysis then considers the constant value and continues with the execution.

Functions on edges:

[x←e]cp#C=C[x↦[e]]cp# [x\leftarrow e]^{\#}_{cp}C = C[x \mapsto [e]]^{\#}_{cp} [x←M[e]]cp#C=C[x↦⊤] [x\leftarrow M[e]]^{\#}_{cp}C = C[x \mapsto \top] [M[e]←x]cp#C=C [M[e]\leftarrow x]^{\#}_{cp}C = C

Abstract Expression Evaluation:

[a⊕b]cp#C=[a]cp#C⊕#[b]cp#C [a \oplus b]^{\#}_{cp}C = [a]^{\#}_{cp}C \oplus^{\#} [b]^{\#}_{cp}C [c]cp#C=c [c]^{\#}_{cp}C = c [x]cp#C=Cx [x]^{\#}_{cp}C = Cx

Here again we use the worklist algorithm to determine the constant values for the set of variables for the program points.

Unreachable Code:

Now we attempt to add another domain to the constant propagation and do unreachable code analysis in cooperation with the constant propagation analysis.

We use unreahability information to refine constantness and constantness to refine unreachability.

Additional domain:

V→{⊤U,⊥U} V \rightarrow \{\top_{U}, \bot_{U}\}

Maps the set of program points to being either reachable (⊤U\top_{U}) or unreachable(⊥U\bot_{U}).

Transfer Functions:

For every reachable program point ∗u∗*u* we can determine the reachability information for vv as follows:

[u→true(e)→v]#C,U=U[v↦⊤U if [e]#C→true] [u \rightarrow true(e) \rightarrow v ]^{\#}C, U = U[v \mapsto \top_{U}\ \text{if} \ [e]^{\#}C \rightarrow \text{true}] [u→false(e)→v]#C,U=U[v↦⊤U if [e]#C→false] [u \rightarrow false(e) \rightarrow v ]^{\#}C, U = U[v \mapsto \top_{U}\ \text{if} \ [e]^{\#}C \rightarrow \text{false}] [u→v]#C,U=U[v↦U[u]] [u \rightarrow v ]^{\#}C, U = U[v \mapsto U[u]]

Domain Cooperation (Unreachable code and Constant Propagation):

When joining over all incoming abstract values in constant propagation we ignore predecessors that are unreachable.

NOTE: We cannot run this analysis one after the another as it wont lead to the optimal analysis. If there is no unreachability information then the constant propagation analysis will be poor which will lead to poor unreachability analysis.

Abstract Interpretation:

Some analysis domain are abstractions of concrete program properties.

Ex: For constant propagation V→(Vars→Z⊥⊤)V \rightarrow (\text{Vars} \rightarrow \mathbb{Z}_{\bot}^{\top})

A state (Σ\Sigma) is a mapping Vars→Z⊥\text{Vars} \rightarrow \mathbb{Z}_{\bot} and exists in concrete program executions.

Ex: For constant propagation {x→⊤,y→42,z→⊥}\{x→\top, y→42, z→\bot\} represents all states where {σ∣σy=42∧σz=⊥}\{\sigma | \sigma y=42 \land \sigma z=\bot \}

We can use this information to prove program analysis correct with respect to the program semantics.

Concretisation(Constant Propagation):

Let S⇒(P(Σ),⊆)\mathbb{S} ⇒ (\mathscr{P}(\Sigma), \subseteq) be the complete lattice.

Let C={Vars→Z⊥⊤,⊑}\mathbb{C} = \{\text{Vars} \rightarrow Z_{\bot}^{\top}, \sqsubseteq\} be the constant propagation lattice.

WE define what an abstract element represents by defining a concretization function γS:C→S\gamma_{\mathbb{S}}: \mathbb{C} \rightarrow \mathbb{S}

γS(C{x↦4})=\gamma_{\mathbb{S}}(C\{x \mapsto 4\}) = Set of all states σ\sigma where Cx↦4Cx \mapsto 4

γS\gamma_{\mathbb{S}} is essentially taking in abstract information and giving us all the states in concrete program execution where the state is valid.

Correctness of edge transformers:

We can use the γ\gamma to define the correctness criterion of the edge transformers.

Each control flow edge has a concrete edge transformer fuv:Σ→Σf_{uv}:\Sigma \rightarrow \Sigma

Each program analysis comes with a domain A\mathbb{A} and an abstract edge transformer (fuv#:A→A)(f_{uv}^\#: \mathbb{A} → \mathbb{A}) for each concrete effect fuvf_{uv}

An abstract edge transformer fuv#f_{uv}^{\#} is correct iff:

fuv(γ(a))⊆γ(fuv#(a))    ∀a∈A f_{uv}(\gamma(a)) \subseteq \gamma(f_{uv}^\#(a)) \ \ \ \ \forall a \in \mathbb{A}

The above statement means that the states that are obtained by a concrete transformation under fuvf_{uv} are a subset of the states that can be obtained after abstract transformation under fuv#f_{uv}^{\#}

To prove the correctness for program analysis transformer we have to prove the above statement.

Finite Prefix Trace Semantics:

A configuration Conf=V×Σ\text{Conf} = V \times \Sigma is a pir of program point and a concrete state*.*

A trace tt is a sequence of configurations.

A finite prefix trace semantics is the set of all program executions.

We can define the following monotone function.

F(T)={(r,σ)∣σ∈Σ} F(T) = \{(r, \sigma) | \sigma \in \Sigma\}

with

(p,σ)→(q,σ′)⟹σ′=fpq(σ) (p, \sigma) → (q, \sigma') \Longrightarrow \sigma' = f_{pq}(\sigma)

for an edge between program point pp with concrete state σ\sigma and qq with concrete state σ′\sigma'

Define the concretisation of constant propagation V→(Vars→Z⊥⊤)V \rightarrow (Vars \rightarrow Z_{\bot}^{\top}) to traces

γ(C)={t∈Conf∗∣(p,σ)∈t⇒σ∈γS(Cp)} \gamma(C) = \{ t \in \text{Conf}^*| (p, \sigma) \in t \Rightarrow \sigma \in \gamma_{\mathbb{S}}(Cp) \}

The above statement means that if given some constant propagation information CC (Vars→Z⊥⊤\text{Vars} \rightarrow Z_{\bot}^{\top}) at a program point pp we can concretize it to a set of trace γ(C)\gamma(C) such that if (p,σ)∈t(p, \sigma) \in t then that state σ\sigma appears in the set of concrete states that are obtained by that constant propagation information at that program point pp; γS(Cp)\gamma_{\mathbb{S}}(C p).

Fixpoint Transfer:

Given a monotone function γ:A→C\gamma:\mathbb{A} \rightarrow \mathbb{C} (A\mathbb{A} abstract, C\mathbb{C} concrete)

and two monotone functions F:C→CF:\mathbb{C} \rightarrow \mathbb{C} and F#:A→AF^{\#}:\mathbb{A} \rightarrow \mathbb{A}

for which the transformer correctness holds: F∘γ⊆γ∘F#F \circ \gamma \subseteq \gamma \circ F^{\#}

then,

lfpF⊆γ(lfpF#) \text{lfp}F \subseteq \gamma(\text{lfp}F^{\#})

Pentagons and Bound Checking:

For array accesses, a programming language like JAVA has to throw an exception if the array access is out of bounds.

So wherever there are array accesses a check is inserted to check the indexing.

There are some cases where the indexing is always in bound and the check is redundant.

With the interval analysis (Vars→(Z×Z)⊥⊤)(\text{Vars} \rightarrow (Z \times Z)_{\bot}^{\top}), we only get the bound [0,∞][0, \infty].

This is not powerful enough to provide the relational information between the variables (i < arr.length\texttt{i < arr.length})

Strict Upper Bounds Domain:

Represents strict inequalities, like x<yx \lt y

Domain: Vars→P(Vars)\text{Vars} \rightarrow \mathscr{P}(\text{Vars})

Maps each variable xx to all the variables that are strictly greater than xx

Concretisation: γsub:s↦{σ∣∀x  y:y∈s(x)⇒σ(x)<σ(y)}\gamma_{\text{sub}} : s \mapsto \{\sigma | \forall x \ \ y : y \in s(x) \Rightarrow \sigma(x) < \sigma(y)\}

Given an element ss from the analysis domain (Vars→P(vars)Vars \rightarrow P(vars)) of SUB the concretisation function γsub\gamma_{sub} maps it to all the states σ\sigma where the value of yy i.e. σ(y)\sigma(y) is strictly greater than the value of xx i.e. σ(x)\sigma(x)

Join: s ⊔sub t: λx.(s(x)∩t(x))s \ \sqcup_{sub} \ t: \ \lambda x.(s(x) \cap t(x))

⊤=λx.∅\top = \lambda x.\varnothing and ⊥=λx.Vars\bot = \lambda x.Vars

the ⊤\top denotes an absence of information about the values of the variables and hence assigns no ordering among the elements.

the ⊥\bot is mapped to an empty state as there are no states where each variable is strictly greater than all other variables at the same time.

γsub(⊤):∑\gamma_{sub}(\top): \sum i.e all the states and γsub(⊥):ϕ\gamma_{sub}(\bot): \phi

Closures: Because <\lt is transitive, there are many element in the SUB that concretise to the same set of states.

s1=[x↦{y},y↦{z}]s_{1} = [x \mapsto \{y\}, y \mapsto\{z\} ] and s2=[x↦{y,z},y↦{z}]s_{2} = [x \mapsto \{y, z\}, y \mapsto\{z\} ]

for both of the above elements of analysis domain we get the same concretisation i.e. γsub(s1)=γsub(s2)\gamma_{sub}(s_{1}) = \gamma_{sub}(s_{2})

Now, because γ\gamma is a monotone function there exists a best abstraction α(c)\alpha(c)

Remember that α(c)\alpha(c) is a function that maps the given set of concrete states to the best possible abstraction.

Therefore (α∘γ)(s)(\alpha \circ \gamma)(s) gives the best abstraction for γ(s)\gamma (s) i.e. the best abstraction that represents the same concrete elements as ss

(α∘γ)(\alpha \circ \gamma) is called the closure operator and is used to obtain the best abstraction for a given concretisation.

Why do I care to get the best abstraction when the concretisation leads to the same concrete states?

It actually makes difference when doing the join operation in the SUB domain.

To make join more precise once could compute the best abstractions using closure and then perform join.

However the closure operator is expensive and requires calculation of the closure of the relation represented by an abstract element

Reduced Product: Using SUB without intervals does not help in proving the array access in bounds. Information about constants is left-out. Hence we use both analysis domains at the same time: pentagons=SUB×intervals\text{pentagons} = \text{SUB} \times \text{intervals}

In the product, there are typically multiple abstract elements that are concretised to the same value:

γ(⟨{x↦[0,100],y↦[0,50]},{x<y}⟩)=γ(⟨{x↦[0,49],y↦[1,50]},{x<y}⟩) \gamma(\langle\{x\mapsto[0, 100], y \mapsto [0, 50]\}, \{x \lt y\}\rangle) = \\ \gamma(\langle\{x\mapsto[0, 49], y \mapsto [1, 50]\}, \{x \lt y\}\rangle)

Therefore, one also gets a closure operator that gives the best abstraction in SUB×interval\text{SUB} \times \text{interval} domain

for a given abstraction

⟨b,s⟩↦⟨b∗,s∗⟩    b∈interval  &  s∈SUB \langle b, s\rangle \mapsto \langle b^*, s^* \rangle \ \ \ \ b \in \text{interval} \ \ \& \ \ s \in \text{SUB} s∗=λx.s(x)∪{y∈Vars∣xu<yl}           b(z)=[zl,zu] s^* = \lambda x.s(x) \cup \{y \in Vars | x^u < y^l \} \ \ \ \ \ \ \ \ \ \ \ b(z) = [z^l, z^u] b∗=⊓{x<y}∈s[x<y]#(b) b^* = \sqcap_{\{x \lt y\} \in s} [ x <y ]^{\#}(b)

In Practice: Applying the closure operator is expensive.

To get the best precision, one has to do it before every operation: join and application of abstract transformer

In practice one uses a less precise but more efficient join. ex: ignore the SUB information for interval join.

Global Value Numbering:

Global Value Numbering ⇒\Rightarrow Entire CFG

Local Value Numbering ⇒\Rightarrow Inside a Basic Block

Local Value Numbering is aka CSE - Common Subexpression Elimination

Value Numbering:

Goal: Eliminate Redundant Computations

Find out if two variables have the same value at a given program point. (Undecidable).

Replace the later computations with the contents of the former.

We resort to Herbrand Equivalence:

We do not consider the interpretations of the operators.

Two expressions are equal if they are structurally equal.

Herbrand Interpretation:

Let TT be the set of terms over a set of variables.

The Herbrand interpretation T\mathscr{T} of an nn-ary operator ω\omega is given as

T(ω):Tn↦TT(ω)(t1,t2,...,tn):=ω(t1,t2,...,tn) \mathscr{T}(\omega): T^n \mapsto T \\ \mathscr{T}(\omega)(t_{1}, t_{2}, ..., t_{n}) := \omega(t_{1}, t_{2}, ..., t_{n})

NOTE: The definition of a term is recursive. each term is composed of a set of terms.

Here the state σ\sigma maps a variable to a term.

σ:Vars↦T \sigma : \text{Vars} \mapsto T

Herbrand Semantics ⟨t⟩σ\langle t \rangle \sigma of a term t as follows

⟨t⟩σ={σ(v)if t = v is a variableT(ω)(⟨t1⟩σ,...,⟨tn⟩σ,)if t =ω(t1,...,tn) \langle t \rangle \sigma = \begin {cases} \sigma(v) &\quad \text{if t = v is a variable} \\ \mathscr{T}(\omega)(\langle t_{1}\rangle \sigma,..., \langle t_{n}\rangle \sigma,) &\quad \text{if t =} \omega(t_{1}, ..., t_{n}) \end{cases}

If the term tt is a variable then the value is equal to the value for that variable in the state σ\sigma

Else we recursively evaluate each term in the expression.

Two terms tt and t′t^{'} are said to be Herbrand equivalent if for all states: ⟨t⟩σ=⟨t′⟩σ\langle t \rangle \sigma = \langle t^{'} \rangle \sigma

Kildall’s Analysis:

Tracks Herbrand Equivalences with a forward analysis.

The equivalence relation is a congruence relation wrt operators in our expression language

For each operator ω\omega, each relation RR and for e1,e2,...∈Vars∪Te_{1}, e_{2}, ... \in \text{Vars} \cup T

e1Re1′  and e2Re2′⇒(e1ωe2)R(e1′ωe2′) e_{1}Re^{'}_{1}\ \ \text{and } e_{2}Re^{'}_{2} \Rightarrow (e_{1} \omega e_{2})R(e^{'}_{1} \omega e^{'}_{2})

Abstract Transformer for an assignment x←tx \leftarrow t

[x←t]#R:={(t1,t2)∣(t1[t/x]) R (t2[t/x])} [x \leftarrow t]^{\#}R := \{ (t_{1}, t_{2}) | (t_{1}[t/x]) \ R \ (t_{2}[t/x]) \}

For the assignment operator x←tx \leftarrow t and a partition RR, the new partition is calculated by relating all the terms t1t_{1} and t2t_{2} where replacing the term xx by tt does not change their equivalence.

Comments on Kildalls Analysis:

Kildalls analysis is sound and complete i.e it discovers all Herbrand equivalences in the program

Straight forward implementation exhibits explosion in the size of equivalence classes.

We can use a better data structure called SED (String Equivalence DAGs) to store the congruences but can still lead to exponential sized graphs.

The Alpern-Wegman-Zadeck Algorithm:

Generates incomplete and less precise value numbering analysis.

Flow insensitive: Does not compute the equivalences for every program point but sound for the whole program

Uses SSA (Static Single Assignment) which is a property of an Intermediate Representation.

In SSA each variable has a single program point where it is defined.

Interpret the SSA data dependence graph as FSM and minimize it.

In constrast to finite automata, we do not create two partitions but a class for every operator symbol.

Note that the ϕ\phi is also treated as an operator.

Two ϕ\phi from different blocks have to be in different classes

Optimistically place all nodes with the same operator symbol in the same class

Find the LFP

Successively split class when two nodes in the class are detected not equivalent