In the previous lesson we discussed the theoretical background which is necessary for abstract interpretation and for the chaotic iteration procedure.
We say that a function ๐ is monotone if ๐ โ ๐ ⇒ ๐(๐) โ ๐(๐)
A Galois connection allows us to do a computation in two domains. The idea is that both domains can be arbitrary and one of them is more abstract. The more concrete domain should be defined as
๐ซ(๐๐ก๐๐ก๐) – the powerset on all states that may occur in a real execution of the program. In this case, we can simplify the definition of abstraction.
We call a pair of functions ๐ผ and ๐พ that connect two domains ๐ด and ๐ถ ( ๐ผ: ๐ถ → ๐ด, ๐พ: ๐ด → ๐ถ ) a Galois connection if:
๏ท ∀๐ ∈ ๐ด, ๐ผ(๐พ(๐)) โ ๐
๏ท
∀๐ ∈ ๐ถ, ๐ โ ๐พ(๐ผ(๐))
๏ท
Both ๐ผ and ๐พ are monotone
We define the functions for a Galois connection from ๐ซ(๐๐ก๐๐ก๐) into A in the following way:
๏ท
We have a state abstraction function, denoted ๐ฝ : State
๏ฎ
A which takes a concrete state to an
abstract state.
๏ท
We have an abstraction function, denoted ๐ผ which takes a group of concrete states to an
abstract state. ๐ผ(๐) =โ {๐ฝ(๐)|๐ ∈ ๐}
Notice that
๏ก
is monotone independent of
๏ข
since if ๐ ⊆ ๐ then ๐ผ(๐) โ ๐ผ(๐) .
๏ท
We have a concretization function, denoted ๐พ which takes an abstract state to a group of
concrete states. ๐พ(๐
#
) = {๐|๐ฝ(๐) โ ๐
# }
By definition, ๐พ is montone, i.e., if ๐ โ ๐′ then ๐พ(๐) = {๐|๐ฝ(๐) โ ๐} โ ๐พ(๐ ′ ) = {๐ ′ |๐ฝ(๐ ′ ) โ ๐ ′ }
๏ท
For the way in which we defined ๐ผ and ๐พ , it must be that they form a Galois connection as we defined above.
We say that a computation is locally sound, if applying each of the statements in the abstract domain, does not yield a more accurate result than abstracting the result of the computation on the concrete domain. Formally
โฆ๐๐ก๐โง #
(๐
#
) โ ๐ผ({โฆ๐๐ก๐โง๐|๐ ∈ ๐พ(๐
#
)})
The soundness theorem allows us to say when the fixed-point of a computation is sound (will be defined below). It states that if:
๏ท
๏ท
๏ท
๏ท
Let (๐ผ, ๐พ) form a Galois connection from ๐ถ (a concrete domain) to ๐ด (an abstract domain)
Let ๐: ๐ถ → ๐ถ be a monotone function
Let ๐ # : ๐ด → ๐ด be a monotone function
Let any of the following conditions hold: o ∀๐ ∈ ๐ด: ๐(๐พ(๐)) โ ๐พ (๐ # (๐))
This condition states that the result of a concrete computation on the concretization of some value, is more accurate than the concretization of the result of the abstract computation. This makes sense, since when doing a computation in the concrete world, we don’t lose information, while an abstract computation may lose lots of information each step and therefore its concretization will be less accurate (will have more “valid” results). o ∀๐ ∈ ๐ถ: ๐ผ(๐(๐)) โ ๐ #
(๐ผ(๐))
This condition states that the abstraction of the outcome from one step of a concrete computation ( ๐ผ(๐(๐)) ) is more accurate ( โ ) than doing a corresponding step in the abstract domain over an abstracted input ( ๐
#
(๐ผ(๐)) ). This is because, again, a computation in the abstract domain loses more info than simply abstracting the final result. o ∀๐ ∈ ๐ด, ๐ผ (๐(๐พ(๐))) โ ๐ # (๐)
Explanation: ๐พ(๐) is the group of all values whose abstraction is the same ( ๐ ) and therefore are undistinguishable in the abstract domain. So, we require that applying an abstraction on the computation on all of them will be at least as accurate as doing the computation in the abstract domain in the first place.
Then the computation of the least-fixed-point is sound, meaning that both of the following properties hold:
๏ท lfp(๐) โ ๐พ (lfp(๐
#
))
This means that the concretization of the result of the abstract computation is less accurate than the result of the actual computation. (We denote โ where on the left side we have the more accurate result because it contains fewer options).
๏ท ๐ผ(lfp(๐)) โ lfp(๐ #
)
This states that the abstraction of the result of a concrete (“real”) computation, is more accurate than the result of doing the entire computation in the abstract domain (which makes sense, like the previous point).
Completeness means one of two options:
1.
๐ผ(lfp(๐)) = lfp(๐
#
)
Computation over the abstract domain is equivalent to doing the computation in the concrete
domain, and then doing an abstraction (i.e., no information is lost for that computation in the abstract domain).
2.
lfp(๐) = ๐พ (lfp(๐
#
))
The concretization of an abstract computation is equivalent to the computation in the real world
(for our specific computation).
This can happen for example, in a concrete domain of [0,1] and an abstraction which is ๐๐๐ ๐๐ก๐๐ฃ๐ and ๐๐๐๐๐๐ ๐๐ก๐๐ฃ๐ . This isn’t pointless, since the abstract computation has a different meaning – it comes to check the property in which we are interested (sign in our case).
This result simply says that the abstract state describes all the reachable states and these only!
It’s hard since it means the abstract domain can be used to describe precisely all the reachable parts in the concrete domain (but not necessarily all the concrete domain) – this is generally not the case.
These conditions are not equivalent. However, condition number 2 under a Galois Insertion implies condition number 1.
Let ๐ผ and ๐พ form a Galois insertion, meaning ๐ผ(๐พ(๐)) = ๐
Then, start with lfp(๐) = ๐พ (lfp(๐ #
))
Apply an abstraction on both sides: ๐ผ(lfp(๐)) = ๐ถ ( ๐ธ (lfp(๐
#
)))
By the Galois Insertion
⇒ ๐ผ(lfp(๐)) = lfp(๐
#
)
QED โ
Constant Propagation is another compiler optimization which can be justified via abstract interpretation. This means that we want to track which values have known constant values at some point of the program. We will define the functions for the Galois connection in the following way: ๐ฝ: [๐๐๐ → โค] → [๐๐๐ → (โค ∪ {โค, ⊥})] ๐ฝ(๐) = ๐ ๐ฝ takes a state (a mapping from variables to โค ) to a state in the abstract interpretation world (a mapping from variables to โค ∪ {โค, ⊥} ) and in this case (constant propagation) gives the variable the same value. ๐ผ: ๐ซ([๐๐๐ → โค]) → [๐๐๐ → (โค ∪ {โค, ⊥})] ๐ผ(๐) =โ {๐ฝ(๐)|๐ ∈ ๐} (= {๐|๐ ∈ ๐})
๐ผ is a mapping from a group of concrete states (where each state is a mapping from variables to values), to a single state in the abstract domain, where in this case (constant propagation) each variable is mapped to the most accurate value as possible using a join ( โ ) operator. Practically this means that if a variable is given several constants (by different states in ๐ ), it will be given โค , otherwise the variable will be given some constant value (if all states agree on one constant or ⊥ ).
Note that if a variable is given the value ⊥ , it means that the variable was never assigned any value. ๐พ(๐
#
) = {๐|๐ฝ(๐) โ ๐
# } (= {๐|๐ โ ๐
# }) ๐พ – is mapping from an abstract state, to the group of all possible concrete states that applying an abstraction on them will give back an abstract state which is more accurate than the current one.
The calculation on expressions is defined recursively in the following way:
๏ท
For a constant ๐ , ๐ดโฆ๐โง๐ = ๐
๏ท
For a variable ๐ , ๐ดโฆ๐ฅโง๐ = ๐(๐ฅ)
๏ท
For a complex operation, we define: o For any operation op ∈ {+, −,∗,/} and it’s actual meaning op
∗
๐ดโฆ๐ op ๐โง๐ = ๐ดโฆ๐โง๐ op
∗
๐ดโฆ๐โง๐
๏ง Except for ๐ดโฆ0 ∗ ๐ฅโง๐ or ๐ดโฆ๐ฅ ∗ 0โง๐ which are both 0
We denote the calculations in the abstract world using ๐ด #
. We calculate atomic expressions in the same way, but we will change the calculation of non-atomic (“complex”) expressions. For complex expressions which only contain constants, nothing is changed. The change will be that ๐ด # โฆโค op
๐โง๐ # and ๐ด # โฆ๐ op โคโง๐ #
evaluate to โค (Except for ๐ด # โฆ0 ∗ โคโง๐ #
or ๐ด # โฆ๐ฅ ∗ โคโง๐ #
which both evaluate to 0 ).
The start state is usually one of the following:
๏ท
All variables are initialized to 0 (or some other known value)
๏ท
All variables are initialized to โค - meaning we expect some unknown random value to be placed in each of the variables (this is often the state in the real world)
๏ท
All variables are initialized to ⊥ - similar to the case with โค , this symbolizes that variables begin with some unknown value. The advantage of putting ⊥ in all of them means that we will be able to detect usage of uninitialized variables and warn about it, in case our analysis discovers that some operation is done where one of the operands is ⊥ .
In that case, when joining two or more control flows (in the chaotic iteration), if in one of them the value of the variable is ⊥ then we will define the join to return ⊥ (unlike the usual join operator that would return the other value!) to symbolize that again the variable may be uninitialized.
We say our computation is locally sound:
โฆ๐๐ก๐โง #
(๐
#
) โ ๐ผ({โฆ๐๐ก๐โง(๐)|๐ ∈ ๐พ(๐
#
)}) =โ (โฆ๐๐ก๐โง(๐)|๐ โ ๐
#
)
This means that an abstract interpretation over an abstract state is less accurate than the abstraction of the computation on an actual state.
3.3.1
Proof of local soundness
The only statement which has any affection on our state is the assignment statement. Obviously, the other statements which do not transform the state (both the abstract and concrete) do not require a proof of soundness.
โฆ๐ฅ โ ๐โง # (๐ # ) = ๐ # [๐ฅ → ๐ด # โฆ๐โง๐ # ], โฆ๐ฅ โ ๐โง(๐) = ๐[๐ฅ → ๐ดโฆ๐โง๐]
We want to prove that
โฆ๐ฅ โ ๐โง #
(๐
#
) โโ (โฆ๐ฅ โ ๐โง(๐)|๐ โ ๐
#
) ๐
# [๐ฅ → ๐ด # โฆ๐โง๐ # ] โโ (๐[๐ฅ → ๐ดโฆ๐โง๐]|๐ โ ๐ #
)
Note that ๐ is a concrete state (so we can apply ๐ด using it) and therefore it has no โค values – only constant values.
For each variable ๐ฆ (where ๐ฆ isn’t ๐ฅ ) there is a value in ๐
#
, and that value can either be some constant or โค . If the value is constant, then for all ๐ โ ๐
#
the value of ๐ฆ in ๐ is the same constant
(by the structure of our lattice). If ๐
# (๐ฆ) = โค , then for all ๐ โ ๐
#
, ๐ฆ will evaluate to some constant
(can be a different constant in each ๐ ). Applying a join ( โ ) operator on all these constants will yield something, and that will be more accurate than โค (by definition of โค ) and so, the order of โ will be preserved.
For ๐ฅ , if ๐ด
# โฆ๐โง๐ #
is โค , then like the rest of the variables, the โ relation will hold. If ๐ด
# โฆ๐โง๐ #
is a constant, then because of our definition to ๐ด
#
, the only 1 way to get a constant is by using a calculation path which has no โค value in it – and that path is defined exactly using ๐ด ’s definition!
Therefore ๐ดโฆ๐โง๐ = ๐ด # โฆ๐โง๐ #
for all ๐ โ ๐ #
So, we saw that the โ relation holds in all cases for all variables.
Q.E.D. โ
The constant propagation we defined is sound (by Cousot’s theorem, local soundness implies global soundness). It also “optimal” when talking about assignments of the form ๐ฅ โ ๐ ( ๐ is a constant or a variable) or ๐ฅ โ ๐ op ๐ where ๐ and ๐ are independent variables or constants. Note that this is an analysis on integers and therefore it does not include division 2 .
1 The only way to receive a constant in a computation with ๐ด #
that includes a โค value is when computing 0 ∗ โค
(or โค ∗ 0 ). In that case, ๐ด #
will evaluate to 0 . In addition, by what we just said, 0 would be computed in a way that ๐ด agrees on with ๐ด # and then applying ๐ด on a multiplication with it will also yield a zero!
Note that if that 0 is also obtained by a recursion with a case like this, it will eventually reach atomic expressions and from there and upward we can construct this agreement of ๐ด and ๐ด # .
2 Note that extending our analysis for division will not be optimal for division statements, since ๐ฅ/๐ฅ should yield 1 for all ๐ฅ , and we don’t do that – if ๐ฅ is โค we return โค .
3.3.2
Proof – Local optimality
Since we already proved soundness (โ ), proving optimality simply requires to prove “accuracy” in the other direction ( โ ). Simply, this means we want to prove that
โฆ๐ฅ โ expโง #
(๐
#
) โโ (โฆ๐ฅ โ expโง(๐)|๐ โ ๐
#
) ๐
# [๐ฅ → ๐ด # โฆexpโง๐ # ] โโ (๐[๐ฅ → ๐ดโฆexpโง๐]|๐ โ ๐ #
)
For each variable ๐ฆ (where ๐ฆ isn’t ๐ฅ ) there is a value in ๐ #
, and that value can either be some constant or โค . If the value is constant, then for all ๐ โ ๐ #
the value of ๐ฆ in ๐ is the same constant
(by the structure of our lattice). If ๐ # (๐ฆ) = โค , then for all ๐ โ ๐ #
, ๐ฆ will evaluate to some constant
(can be a different constant in each ๐ ). Since we can choose two or more different constants (since we are talking about all states ๐ more accurate than ๐ #
), applying a join ( โ ) operator on all these constants will yield โค .
So we showed that for each variable ๐ฆ different than ๐ฅ , the value of ๐ฆ on both sides is the same.
For ๐ฅ itself, we need to do the separation:
3.3.2.1
๐ โ ๐ (constant or variable) if ๐ด
# โฆ๐โง๐ #
is a constant, then like the rest of the variables, the โ relation will hold. If ๐ด
# โฆ๐โง๐ #
is โค
(only possible when ๐ is a variable), then ๐ may evaluate to any constant value in ๐ , and so in the join we will get a join on many constants – yielding โค and preserving the โ relation.
3.3.2.2
๐ โ ๐ op ๐ (for two independent variables/constants) if ๐ด # โฆ๐ op ๐โง๐ #
is a constant, then like we showed in the soundness proof, there will be an equality for the value of ๐ฅ on both sides of the โ operator. if ๐ด
# โฆ๐ op ๐โง๐ #
evaluates to โค , then one of the following holds (note that operators are symmetric when it comes to yielding โค ):
๏ท
๐ด
# โฆ๐ op ๐โง๐ #
where op ∈ {+, −} and W.L.O.G ๐ด
# โฆ๐โง๐ #
= โค
In that case we can choose ๐
1
, ๐
2
โ ๐
#
where ๐ดโฆ๐โง๐
1
= 1 and ๐ดโฆ๐โง๐
2
= 2 , and so in the join we will receive a join on two different values of ๐ op ๐ ! This means that the result on the right hand-side of the above identity will also be โค !
๏ท
๐ด # โฆ๐ op ๐โง๐ #
where op ∈ {∗} and W.L.O.G ๐ด # โฆ๐โง๐ # = โค and ๐ด # โฆ๐โง๐ # ≠ 0
Similar to the previous case…
๏ท
Note that ๐ด # โฆ๐ op ๐โง๐ # = โค where op ∈ {∗} and W.L.O.G ๐ด # โฆ๐โง๐ # simply can’t happen – because of how we defined ๐ด #
on ∗ .
= โค and ๐ด # โฆ๐โง๐ # = 0
Q.E.D. โ
3.3.3
A word about independence
Our analysis isn’t always optimal – in fact, it can miss several cases of constants. For example: ๐ฅ − ๐ฅ
This would always evaluate to 0 in the concrete computation, but our analysis may give it a โค when ๐ # (๐ฅ) = โค . There can also be more complex examples.
The point is that dependency between variables introduces a new difficulty which is not trivial to solve in order to achieve optimality, and therefore it’s outside the scope of this summary.
We say it’s not complete, because doing an abstraction over the result of the actual computation does not necessarily yield the same result as doing an abstract computation in the first place. For example:
1.
Y = 1;
2.
X = (1 or 2);
3.
If (X % 2 == 0)
4.
Y = 4;
5.
Do some stuff without X...
6.
If (X % 2 == 1)
7.
Y = 4;
Unless our analysis will be smart enough to figure that lines 3 and 5 are opposite conditions that can’t change between checks, it will not figure out that Y will always finish as 4 – after line 4 Y will be
โค and it will finish like this. So our analysis is not complete. lfp(๐
#
) = [๐ฅ → โค, ๐ฆ → โค], lfp(๐) = {[๐ฅ → 1, ๐ฆ → 4], [๐ฅ → 2, ๐ฆ → 4]}
And as we can see, both completeness properties don’t hold: ๐ผ(lfp(๐)) = [๐ฅ → โค, ๐ฆ → 4] โ [๐ฅ → โค, ๐ฆ → โค] = lfp(๐ #
) lfp(๐) = {[๐ฅ → 1, ๐ฆ → 4], [๐ฅ → 2, ๐ฆ → 4]} โ
(⊂)
{[๐ฅ → ๐, ๐ฆ → ๐]|๐, ๐ ∈ โค} = ๐พ (lfp(๐ #
))
Here we try to analyze which formal expressions are available at each step and do not require recomputation (this is useful for caching results instead of computing them again). A state is “good” if we have many available expressions, and it’s “worse” if it has less available expressions.
Sentence ๐ฅ = ๐ฆ + ๐ก ๐ง = ๐ฆ + ๐ while(… ) { ๐ก = ๐ก + (๐ฆ + ๐)
}
Expressions available after computing the current sentence
{(๐ฆ + ๐ก)}
{(๐ฆ + ๐ก), (๐ฆ + ๐)}
{(๐ฆ + ๐ก), (๐ฆ + ๐)}
{(๐ฆ + ๐)} (note – the original slides had a mistake)
Line 4 requires a bit of explanation, so let’s understand what happened there, step by step (a formal explanation is available on the next slide):
We began with the available expressions from the last step {(๐ฆ + ๐ก), (๐ฆ + ๐)}
Then we computed a new expression ๐ก + (๐ฆ + ๐) and added it to the group of available expressions
Then we assigned a new value for ๐ก and by that invalidated all expressions which used the former value of ๐ก (including ๐ก + (๐ฆ + ๐) which we just computed), and therefore we remained only with {(๐ฆ + ๐)}
๏ท ๐ซ(๐น๐๐ฅ๐)
We denote all the program expressions as ๐น๐๐ฅ๐ , and the domain we are working over is the power set of all the program expressions.
๏ท
๐ โ ๐ ⇔ ๐ ⊇ ๐
The notation โ which we used previously to denote a state as “more accurate” is now used to define which state has more available expressions. I.E. ๐ โ ๐ means ๐ is a “better” state than ๐ since it contains at least all the expressions which were available on ๐ .
๏ท
๐ โ ๐ = ๐ ∩ ๐
The lowest upper bound, which previously meant “the most accurate value that is less accurate than all the given ones” is now the intersection of the available expressions – this is as “good” as a state can be, while still being “worse” than the given ones.
๏ท ⊥= ๐น๐๐ฅ๐
The “best” state in which we can be, is when all the program expressions (the group ๐น๐๐ฅ๐ ) are available
๏ท
โค = ∅
The worst state is when we have nothing available.
Here we define the computation for our interpretation – this list shows which expressions may be computed (at some potential execution flow) for each statement. This is useful in cases where we want to acquire the list of possible expressions before starting the iteration, for building a finite lattice 3 .
๏ท ๐ โท= skip
Obviously, the skip statement does nothing, and therefore computes nothing ( ๐ . ๐น๐๐ฅ๐ = ∅ ).
๏ท ๐ โท= ๐๐ โ exp
In order to compute an assignment statement, we need to computer the expression which is to be assigned to a variable ( ๐ . ๐น๐๐ฅ๐ = exp. ๐๐๐ 4 )
๏ท ๐ โถโ ๐
1
; ๐
2
In order to compute two following sentences, we must compute each of the sentences
( ๐ . ๐น๐๐ฅ๐ = ๐
1
. ๐น๐๐ฅ๐ ∪ ๐
2
. ๐น๐๐ฅ๐ ).
๏ท ๐ โท= while exp do ๐
1
In order to compute a while statement, we must compute the Boolean expression and the following statement, so the total list of expressions that needs to be computed is the union of the ones for the Boolean condition and the ones for the statement (s . ๐น๐๐ฅ๐ = exp. ๐๐๐ ∪ ๐
1
. ๐น๐๐ฅ๐ )
3 So in other words, if we agree to start with an empty lattice and add expressions as we encounter them, we can skip this step of passing over all the statements and collecting expressions
4 We denote the actual expressions to compute from exp as exp. ๐๐๐
๏ท ๐ โท= if exp then ๐ else ๐
Similar to the while statement, we may need to compute the expression and both statements.
(s . ๐น๐๐ฅ๐ = exp. ๐๐๐ ∪ ๐
1
. ๐น๐๐ฅ๐ ∪ ๐
2
. ๐น๐๐ฅ๐ )
We need to explain which expressions are available after each statement. In order to do this formally, we define an instrumented semantics in which a “state” is composed of “variable states”
( ๐ ) and “available expressions” ( ae ). At each step of the computation we begin with a step (๐
0
, ae
0
) and we define how the current statement ๐๐ก๐ transforms that state into (๐
1
, ae
1
) .
Formally, we mark that as
๐โฆ๐๐ก๐โง: ๐๐ก๐๐ก๐ × ๐ซ(๐น๐๐ฅ๐) → ๐๐ก๐๐ก๐ × ๐ซ(๐น๐๐ฅ๐)
For an assignment we denote
๐โฆ๐๐ โถ= ๐โง(๐,ae) = (๐[๐๐ → ๐ดโฆ๐โง๐] , ๐๐๐ก๐ด๐๐(๐๐, ๐๐ ∪ {๐}))
This means that for an assignment statement, the new state is composed out of:
๏ท
The old variable state updated with the new value for the variable
๏ท
The old list of available expressions combined with the newly computed expression ๐ , and from that we remove all the expressions in which ๐๐ was an argument. We do that since the assignment to ๐๐ that was done at the end, invalidated all the expressions that depend on ๐๐ ( ๐๐ now has a new value which means we need to re-compute them).
For a skip statement, nothing has to be done, so the old state (before executing the skip statement) is the same as the new state. We denote this as
๐โฆskipโง(๐,ae) = (๐,ae)
After we defined the semantics ( ๐โฆ๐๐ก๐โง ) for each statement which has a meaning other than control flow (assignment and skip), we now need to define the Collecting Semantics. The Collecting
Semantics takes a group of states and a statement, and returns a group of possible result states from applying the given statement on any of the states.
๐ถ๐โฆ๐๐ก๐โง: ๐ซ(๐๐ก๐๐ก๐ × ๐ซ(๐น๐๐ฅ๐)) → ๐ซ(๐๐ก๐๐ก๐ × ๐ซ(๐น๐๐ฅ๐))
๐ถ๐โฆ๐๐ก๐โง(๐) = {๐โฆ๐๐ก๐โง(๐,ae)|(๐,ae) ∈ ๐}
Let’s look at an example of collecting semantics, were we begin with the state
Sentence ๐ฅ = ๐ฆ ∗ ๐ง; if (๐ฅ == 6) ๐ฆ = ๐ง + ๐ก;
([๐ฆ → 2, ๐ง → 3, ๐ฅ → 0, ๐ก → 10, ๐ → 0], ∅)
Concrete state after computing this statement
([๐ฆ → 2, ๐ง → 3, ๐ฅ → 0, ๐ก → 10, ๐ → 0], ∅)
([๐ฆ → 2, ๐ง → 3, ๐ฅ → 6, ๐ก → 10, ๐ → 0], {๐ฆ ∗ ๐ง})
([๐ฆ → 13, ๐ง → 3, ๐ฅ → 6, ๐ก → 10, ๐ → 0], {๐ง + ๐ก})
Abstracted State
∅
{๐ฆ ∗ ๐ง}
{๐ง + ๐ก}
… if (๐ฅ == 6) ๐ = ๐ง + ๐ก; ([๐ฆ → 13, ๐ง → 3, ๐ฅ → 6, ๐ก → 10, ๐ → 13], {๐ง + ๐ก}) {๐ง + ๐ก}
Now we will define the abstract interpretation for Available Expressions like we did with constant propagation:
First we define ๐ฝ: [๐๐๐ → โค] × P(Fexp) → P(Fexp) , the abstract interpretation of a single concrete state (composed of variable states and a group of available expressions). The abstract result is a group of available expressions derived from the concrete state: ๐ฝ(๐, ๐๐) = ๐๐
Then we define ๐ผ:P([๐๐๐ → โค] × P(Fexp)) → P(Fexp) , the abstract interpretation of a group of concrete states. The abstract result is a group of available expressions derived from all the concrete states together – meaning the expressions all of the states agree on: ๐ผ(X) = โจ{๐ฝ(๐)|๐ ∈ X} = ∩ {๐๐|(๐, ๐๐) ∈ X}
Finally, we define ๐พ:P(Fexp) → P([๐๐๐ → โค] × P(Fexp)) , the concretization of an abstract state.
The result is a group of concrete states which their abstract interpretation is “better” (as we defined before) than the given abstract state: ๐พ(๐๐
#
) = {(๐, ๐๐)|๐ฝ(๐, ๐๐) โ ๐๐
# } = {(๐, ๐๐)|๐๐ ⊇ ๐๐ #
}
Now we will define the abstract semantics for Available Expressions.
For each statement, we show how it affects the state in the abstract interpretation:
Sโฆ๐๐ก๐โง
#
: P(Fexp) → P(Fexp)
๏ท
๏ท
For the skip statement the available expressions stay the same:
Sโฆ๐ ๐๐๐โง # (๐๐)= ae
For an assignment we add the calculated expression, and then remove all expressions containing the variable we’ve just changed (because the assignment has just invalidated them):
Sโฆ๐๐ โ ๐โง
#
(๐๐
#
) = ๐๐๐ก๐ด๐๐(๐๐ , ๐๐
#
∪ {๐})
๏ท
All other statements in the while language are irrelevant because they determine only the control flow of the program and have no actual meaning. These will only matter for determining the “neighbors” of each statement when doing the Chaotic Iteration, as we did in the previous lessons.
Now, let’s see an example abstract interpretation (for the previous program):
Sentence
Abstract state after the current statement
∅
Explanation
We begin with nothing computed
๐ฅ = ๐ฆ ∗ ๐ง; if (๐ฅ == 6) ๐ฆ = ๐ง + ๐ก;
… if (๐ฅ == 6) ๐ = ๐ง + ๐ก;
{๐ฆ ∗ ๐ง}
{๐ง + ๐ก}
We now begin an optional execution branch
{๐ง + ๐ก}
{๐ฆ ∗ ๐ง} โ {๐ง + ๐ก} = ∅ When finishing the branch, merge it back with the main using a join operator
We now begin an optional execution branch
Note that the Collecting Semantics is much more accurate (in the CS, at each state we had at least the same expressions available if not more) than our analysis – our analysis will only compute available expressions (unlike the CS which also tracks the variable states) and so it won’t know that we will definitely enter the first “if” statement, making the expression ๐ง + ๐ก available in the second
“if” statement. In the AI, we can see that we had an ∅ of available expressions, while in the CS (1-2 pages ago) exactly at the same place, we had one available expression! Since our AI will miss an available expression, it is not complete!
๏ท
Local Soundness:
โฆ๐ ๐กโง #
(๐๐
#
) โ โ {(โฆ๐ ๐กโง(๐,ae))[1]|ae โ ae
#
}
โฆ๐ ๐กโง # (๐๐ # ) ⊆ ∩ {(โฆ๐ ๐กโง(๐,ae))[1]|ae ⊇ ae # }
Note that โฆ๐ ๐กโง(๐,ae) = (๐
1
, ae
1
) and the [1] at the end is like an array reference, which is meant to return ae
1
4.9.1
Proof
We will prove the local soundness for all types of statements for which we defined some meaning:
Sโฆ๐ ๐๐๐โง
#
(๐๐
#
) = ae
#
๐โฆskipโง(๐,ae) = (๐,ae)
∩ {(โฆ๐ ๐๐๐โง(๐,ae))[1]|ae ⊇ ae
#
} =∩ {(๐,ae)[1]|ae ⊇ ae
# }
=∩ {ae|ae ⊇ ae
# ae
#
=
โ
โฆ๐ ๐๐๐โง #
(๐๐
#
)
* As we can see, in the intersection we can take ae ≡ ae
#
(since ae
# them all, we get the smallest group which is in fact ae #
.
⊇ ae
#
) and then we intersect
Sโฆ๐๐ โ ๐โง
#
(๐๐
#
) = ๐๐๐ก๐ด๐๐(๐๐ , ๐๐
#
∪ {๐})
๐โฆ๐๐ โถ= ๐โง(๐,ae) = (๐[๐๐ → ๐ดโฆ๐โง๐] , ๐๐๐ก๐ด๐๐(๐๐, ๐๐ ∪ {๐}))
First of all let’s show that ๐๐๐ก๐ด๐๐ is monotone (in the expression list). Let ae
1
and ae
2
be two expression lists, where ae
1
⊆ ae
2
. So ๐๐๐ก๐ด๐๐(๐๐, ae
2
) = ๐๐๐ก๐ด๐๐(๐๐, ae
1
∪ (๐๐
2
⊇ ๐๐๐ก๐ด๐๐(๐๐, ae
1
)
− ๐๐
1
)) = ๐๐๐ก๐ด๐๐(๐๐, ae
1
) ∪ ๐๐๐ก๐ด๐๐(๐๐, ๐๐
2
− ๐๐
1
)
∩ {(โฆ๐๐ โ ๐โง(๐,ae))[1]|ae ⊇ ae
#
} = ∩ {((๐[๐๐ → ๐ดโฆ๐โง๐] , ๐๐๐ก๐ด๐๐(๐๐, ๐๐ ∪ {๐}))) [1]|ae ⊇ ae
#
}
= {๐๐๐ก๐ด๐๐(๐๐, ๐๐ ∪ {๐})|ae ⊇ ae
# } =
By monotonicity of ๐๐๐ก๐ด๐๐ we take the "smallest" elemnt
= Sโฆ๐๐ โ ๐โง
#
(๐๐
#
) ๐๐๐ก๐ด๐๐(๐๐, ๐๐
#
∪ {๐})
We proved local soundness for all statements for which we defined an interpretation (for statements such as while, for which we defined no interpretation, it means that in the AI they serve as identity transforms), and therefore following Cousot’s theorem we gain global soundness. โ
๏ท
Optimality
โฆ๐ ๐กโง #
(๐๐
#
) = {๐ผ(โฆ๐ ๐กโง(๐,ae))|๐ ∈ ๐พ(๐
#
)} = โ {(โฆ๐ ๐กโง(๐,ae))[1]|๐ โ ๐
#
}
In the proof of local soundness, we had an equality and not a โ , meaning that our AI is indeed locally optimal. โ
๏ท
A variable ๐ฅ may-be-garbage at a program point v if there exists an execution path leading to v which ๐ฅ ’s value is unpredictable. We say that a value is unpredictable if one of the following conditions hold:
(1) Was not assigned
(2) Was assigned using an unpredictable expression
๏ท
We will define the Lattice as following: o ๐ฟ ๐๐๐
= ๐ซ(๐๐๐)
The group of all unpredictable variables. o โ = ⊆
A state is “more accurate” if less variables are unknown, so we define “more accurate”=”list of unknown variables is contained in the other one” o ⊥ = ∅
The most accurate state is when no variables are unknown o โค = ๐๐๐
The least accurate state is when all variables are unknown o โ = ∪
The most accurate state which is less accurate than two given states, is when the group of unknown variables is the union of the unknown groups of each state o โ = ∩
Similar to โ=∪ o Initial state is ๐๐๐
All variables are NOT assigned
๏ท
The abstract interpretation: o ๐ฝ: [๐๐๐ → โค] × P(๐๐๐) → P(๐๐๐) ๐ฝ(๐, ๐๐๐) = ๐๐๐ o ๐ผ and ๐พ are defined accordingly.
๏ท
The abstract semantics: [arg(exp) means the arguments in the expression) o โฆ๐ฅ โ ๐๐ฅ๐โง #
(๐๐๐
#
) = { ๐๐๐ # ๐๐๐ #
−{๐ฅ} , ๐๐ arg(๐๐ฅ๐)∩๐๐๐ #
∪{๐ฅ} , ๐๐๐ ๐
=∅
๏ท
Galois Connection
5.1.1
Proof
Let ๐ ๐๐๐
be a group of variables (representing the may-be-garbage variables) ๐ผ (๐พ(๐ ๐๐๐
)) = ๐ผ ({(๐, ๐
′ ๐๐๐
′ ๐๐๐
) ๐
′ ๐๐๐
โ ๐ ๐๐๐
⇒ ๐ผ (๐พ(๐ ๐๐๐
)) โ ๐ ๐๐๐
Let ๐ ๐๐๐
= {(๐ ๐
, ๐ ๐๐๐ ๐
)} ๐ ๐=1
be a group of concrete states
′ ๐๐๐
) ๐
′ ๐๐๐
|๐
′ ๐๐๐
โ ๐ ๐๐๐
} โ ๐ ๐๐๐ ๐พ(๐ผ(๐)) = ๐พ (๐ผ ({(๐ ๐
, ๐ ๐๐๐ ๐ ๐
)} ๐=1 ๐
)) = ๐พ ( โ ๐=1 ๐ฝ (๐ ๐
, ๐ ๐๐๐๐
) ๐ ๐๐๐๐
= {(๐, ๐
′ ๐๐๐
′ ๐๐๐
) ๐
โ โ ๐=1 ๐ ๐๐๐๐
} = {(๐, ๐
′ ๐๐๐
)|∀๐, ๐
′ ๐๐๐
= {(๐, ๐
′ ๐๐๐
)|∀๐, ๐ ๐
′ ๐๐๐
′ ๐๐๐ ๐
⊆ ∪ ๐=1 ๐ ๐๐๐๐
} ⊇ {(๐ ๐
, ๐ ๐๐๐ ๐ ๐
)} ๐=1 ๐
โ โ ๐=1
⇒ ๐ โ ๐พ(๐ผ(๐)) ๐ ๐๐๐๐
}
We can easily see that ๐ผ and ๐พ are monotone, since ๐ฝ does not change the state – it only takes its second field.
๏ท
Soundness
5.1.2
Proof
โฆ๐ฅ โ ๐๐ฅ๐โง(๐, ๐๐๐) = (๐[๐ฅ → ๐ดโฆ๐๐ฅ๐โง๐], { ๐๐๐−{๐ฅ} , ๐๐ arg(๐๐ฅ๐)∩๐๐๐=∅ ๐๐๐∪{๐ฅ} , ๐๐๐ ๐
)
โฆ๐ฅ โ ๐๐ฅ๐โง # (๐๐๐ # ) = { ๐๐๐
# ๐๐๐
−{๐ฅ} , ๐๐ arg(๐๐ฅ๐)∩๐๐๐
#
=∅
#
∪{๐ฅ} , ๐๐๐ ๐
โ {(โฆ๐ฅ โ ๐๐ฅ๐โง(๐,gar))[1]|gar โ gar
#
} =∪ {(โฆ๐ฅ โ ๐๐ฅ๐โง(๐,gar))[1]|gar ⊆ gar
#
}
=∪ {{ ๐๐๐−{๐ฅ} , ๐๐ arg(๐๐ฅ๐)∩๐๐๐=∅ ๐๐๐∪{๐ฅ} , ๐๐๐ ๐
|gar ⊆ gar
#
} =
=∪ {{ ๐๐๐−{๐ฅ} , ๐๐ arg(๐๐ฅ๐)∩๐๐๐=∅ ๐๐๐∪{๐ฅ} , ๐๐๐ ๐
|gar ⊆ gar
#
} = ($)
Case 1: arg(๐๐ฅ๐) ∩ ๐๐๐ # = ∅ (which implies arg(๐๐ฅ๐) ∩ ๐๐๐ = ∅ for gar ⊆ gar #
)
($) =∪ {๐๐๐ − {๐ฅ}|gar ⊆ gar # } = gar #
− {๐ฅ} = { ๐๐๐ # ๐๐๐
−{๐ฅ} , ๐๐ arg(๐๐ฅ๐)∩๐๐๐ #
#
∪{๐ฅ} , ๐๐๐ ๐
=∅
= โฆ๐ฅ โ ๐๐ฅ๐โง #
(๐๐๐
#
)
Case 2: Else (not case 1) (which implies ๐๐๐ # ∪ {๐ฅ} will be in the union, when ๐๐๐ = ๐๐๐ #
)
# } ∪ (๐๐๐
#
∪ {๐ฅ}) = ๐๐๐
#
∪ {๐ฅ} = { ๐๐๐
# ๐๐๐
#
−{๐ฅ} , ๐๐ arg(๐๐ฅ๐)∩๐๐๐
#
∪{๐ฅ} , ๐๐๐ ๐
=∅
⊆๐๐๐ # ∪{๐ฅ}
= โฆ๐ฅ โ ๐๐ฅ๐โง
#
(๐๐๐
#
)
We can see that in both cases โ {(โฆ๐ฅ โ ๐๐ฅ๐โง(๐,gar))[1]|gar โ gar #
} =
โ
โฆ๐ฅ โ ๐๐ฅ๐โง #
(๐๐๐
#
) . โ
๏ท
Optimality – In the proof of local soundness, we had an equality and not a โ , meaning that our
AI is indeed locally optimal. โ
๏ท
Not complete o Similarly to the constant propagation, not tracking concrete values can miss control flows that will initialize things
We will begin by defining the syntax of the new language ๐๐ฅ๐ โถโ ๐ฅ | * ๐ฅ | & ๐ฅ |๐ | ๐๐ฅ๐
1
๐๐ ๐๐ฅ๐
๐๐ฅ๐
2 ๐๐๐๐ โถโ true | false | not ๐๐๐๐ | ๐๐๐๐
1 ๐๐ ๐๐๐๐ ๐๐๐๐
2 ๐ ๐ก๐๐ก โถโ ๐ฅ โ ๐๐ฅ๐ | *๐ฅ โ exp | skip | ๐ ๐ก๐๐ก
1
; ๐ ๐ก๐๐ก
2
| if ๐๐๐๐ then ๐ ๐ก๐๐ก
1
else ๐ ๐ก๐๐ก
2
| while ๐๐๐๐ do ๐ ๐ก๐๐ก
In PWhile, we changed our previous definition of states. Instead of having a mapping from a variable to its value, we define a mapping from a location (which corresponds to the (memory) location of that variable) to another location or (integer) value: ๐ ๐ก๐๐ก๐: ๐ฟ๐๐ → ๐ฟ๐๐ ∪ โค
For every atomic statement S we define the semantic function:
โฆSโง: ๐ ๐ก๐๐ก๐ → ๐ ๐ก๐๐ก๐
๏ท
We define loc(x) as the location of variable x.
๏ท โฆ๐ฅ โ ๐โง(๐) = ๐[loc(๐ฅ) → Aโฆ๐โง๐ ] where: o For a constant ๐ , Aโฆ๐โง๐ = ๐ o For a variable ๐ฆ , Aโฆ๐ฆโง๐ = ๐(loc(๐ฆ)) o For a variable ๐ฆ , Aโฆ&๐ฆโง๐ = loc(๐ฆ)
Meaning in an assignment the location of x will change to the value in ๐ of the location of y. (it is possible its value is another location) o For a variable ๐ฆ , Aโฆ*๐ฆโง๐ = ๐ (๐(loc(๐ฆ)))
Meaning in an assignment the location of x will change to the value of the value in ๐ of the location of y. o Intuition: &y is the address (location) of y, y is the value of y, and *y is the value of y’s inner address.
Meaning in an assignment the location of x will change to the location of y. o Aโฆ๐๐ฅ๐
1
๐๐ ๐๐ฅ๐
๐๐ฅ๐
2
โง๐ = Aโฆ๐๐ฅ๐
1
โง๐ ๐๐ ๐๐ฅ๐
Aโฆ๐๐ฅ๐
2
โง๐
๏ท โฆ*๐ฅ โ ๐โง(๐) = ๐[๐(loc(๐ฅ)) → Aโฆ๐โง๐]
Meaning the inner value of x (the value of the location it points to) will change to the semantics of ๐ (as defined in A ).
Basically, we need this for every analysis on a language which supports pointers. For example, if in C we would like to do constant propagation, we would want to know when assigning through pointers, which variables may be affected. Since our points-to analysis may produce more pointers than the actual pointing-state, whenever an assignment through a pointer does not agree with the previous value, we will put a โค on that variable. This will allow us to keep the soundness of the constant propagation, since we won’t generate more constants than any concrete run.
The point of this analysis is not to miss any points-to relation between two variables (such as ๐ฅ points at ๐ฆ ) and we define a state as “more accurate”/”better” if it has “less” point-to relations.
๏ท
We will define the Lattice L pt
as following: o L = P(๐๐๐ × ๐๐๐)
The lattice is composed of the group of all variable pairs (the first one points to the second) o โ = ⊆
A state is “more accurate” if it’s group of point-to pairs is contained inside the group of the other state. o โค = ๐๐๐ × ๐๐๐
When we don’t know anything, assume all variables are pointing at all the others, since we don’t want to miss any points-to pair. o ⊥ = ∅
The most accurate state in this analysis (where we don’t want to miss any point-to pair) is when we know nothing points at nothing. o โ = ∪
When we need to find the most accurate state which is less accurate than all given states, create a union of the points-to pairs, to make sure we are least accurate than all given states o โ = ∩
Similar logic to โ , but reversed
๏ท
Abstraction: ๐ฝ: (๐ฟ๐๐ → ๐ฟ๐๐ ∪ โค) → P(๐๐๐ × ๐๐๐)
The ๐ฝ function takes a state ๐ and returns the group of all pairs of variables <x,y>, which the value of the location of x is the location of y (meaning that x points to y): ๐ฝ(๐) = {(๐ฅ, ๐ฆ)|๐(loc(๐ฅ)) = loc(๐ฆ)}
The ๐ผ function takes a group of states and returns the union of the abstraction of each state
(meaning all the “variable points-to” option that exists in them): ๐ผ: ๐ซ(๐ฟ๐๐ → ๐ฟ๐๐ ∪ โค) → P(๐๐๐ × ๐๐๐) ๐ผ(๐ด) = ∪ {(๐ฅ, ๐ฆ)|๐ ∈ ๐ด ∧ ๐(loc(๐ฅ)) = loc(๐ฆ)}
Let ๐
#
be an abstract state (i.e., a group of point-to pairs). We will now analyze the abstract semantics for assignment statements:
๏ท โฆ ๐ โ ๐โง
# (๐๐ก) = ๐๐ก − {(๐, ๐)|๐ ∈ ๐ฝ๐๐}
If we assign a constant ๐ to a variable ๐ฅ , remove all the pairs in which ๐ฅ points at some other variable
๏ท โฆ ๐ โ ๐ โง # (๐๐ก) = (๐๐ก − {(๐, ๐)|๐ ∈ ๐ฝ๐๐} ) ∪ {(๐, ๐)|(๐, ๐) ∈ ๐๐}
If we assign the value of a variable ๐ฆ to a variable ๐ฅ , like before remove all the pairs in which ๐ฅ points at some other variable, and for each pair that ๐ฆ points at ๐ค , add a pair of ๐ฅ pointing at ๐ค
๏ท โฆ ๐ โ &๐ฆ โง # (๐๐ก) = (๐๐ก − {(๐, ๐)|๐ ∈ ๐ฝ๐๐} ) ∪ {(๐, ๐)}
If we assign the location of the variable ๐ฆ to a variable ๐ฅ , remove all the pairs in which ๐ฅ points at some other variable, and add a pair saying that ๐ฅ points to ๐ฆ
๏ท โฆ ๐ โ *๐ โง # (๐๐ก) = (๐๐ก − {(๐, ๐)|๐ ∈ ๐ฝ๐๐} ) ∪ {(๐, ๐)|(๐, ๐), (๐, ๐) ∈ ๐๐}
If we assign the value pointed by a variable ๐ฆ to a variable ๐ฅ , like before remove all the pairs in which ๐ฅ points at some other variable, and for each pair that ๐ก points at ๐ค and ๐ฆ points at ๐ก , add a pair of ๐ฅ pointing at ๐ค
๏ท
โฆ *๐ โ ๐ โง # (๐๐ก) = (๐๐ก − {(๐, ๐)|๐ ∈ ๐ฝ๐๐, (๐, ๐) ∈ ๐๐ and ∀๐, (๐, ๐) ∈ ๐๐ ⇒ ๐ = ๐} ) ∪
{ (๐, ๐) | (๐, ๐) , (๐, ๐) ∈ ๐๐ก}
If we assign the value of a variable ๐ฆ to the variable pointed by ๐ฅ (we mark the variable pointed by ๐ฅ as ๐ค ), if ๐ฅ indeed points at most at one variable ๐ค (This is the condition
∀๐ง, (๐ฅ, ๐ง) ∈ ๐๐ก ⇒ ๐ง = ๐ค ) then remove all the pairs in which ๐ค points at some other variable.
Then for each pair that ๐ฆ points at ๐ , add a pair of ๐ค pointing at ๐ .
It wouldn’t be sound to delete the group of pointers of the form (๐ค, ๐) when ๐ฅ points at more than one variable ๐ค . Why? We’ll demonstrate below.
๏ท
Continuing โฆ*๐ฅ โ *๐ฆโง , โฆ*๐ฅ โ ๐โง and โฆ*๐ฅ โ &๐ฆโง is done in a similar way
โฆ*๐ โ ๐โง
Why would it be unsound to delete the pointer group
{(๐, ๐)|๐ ∈ ๐ฝ๐๐, (๐, ๐) ∈ ๐๐} when ๐ฅ points at more than one variable in our analysis?
Let’s begin with some concrete state in which ๐ฅ > 0 on the following example
Sentence
Concrete y := &b;
∅
{(๐ฆ, ๐)}
{(๐ฆ, ๐), (๐ง, ๐)} z := &c; if x>0
then x:= &y; {(๐ฆ, ๐), (๐ง, ๐), (๐ฅ, ๐ฆ)} state after the current statement
Abstract
∅
{(๐ฆ, ๐)}
{(๐ฆ, ๐), (๐ง, ๐)}
{(๐ฆ, ๐), (๐ง, ๐), (๐ฅ, ๐ฆ)}
else x:= &z; Not executed
{(๐ฆ, ๐), (๐ง, ๐), (๐ฅ, ๐ฆ)}
{(๐ฆ, ๐), (๐ง, ๐), (๐ฅ, ๐ง)}
{(๐ฆ, ๐), (๐ง, ๐), (๐ฅ, ๐ฆ)} โ {(๐ฆ, ๐), (๐ง, ๐), (๐ฅ, ๐ง)}
= {(๐ฆ, ๐), (๐ง, ๐), (๐ฅ, ๐ฆ), (๐ฅ, ๐ง)}
({(๐ฆ, ๐), (๐ง, ๐), (๐ฅ, ๐ฆ), (๐ฅ, ๐ง)} − {(๐, ๐), (๐, ๐)} )
*x := &t; {(๐ฆ, ๐ก), (๐ง, ๐) , (๐ฅ, ๐ฆ), (๐ฅ, ๐ง)} ∪ {(๐ฆ, ๐ก), (๐ง, ๐ก)}
= {(๐ฆ, ๐ก), (๐ง, ๐ก), (๐ฅ, ๐ฆ), (๐ฅ, ๐ง)}
We defined the accuracy of our analysis by forcing it to include at least all the pointers that the concrete computation will have, but it missed the point-to pair (๐ง, ๐) ! Therefore, it’s not sound.
Sentence t := &a; y := &b; z := &c; if x>0
then p:= &y;
else p:= &z;
*p := t;
Abstract state after the current statement
∅
Explanation
We begin with nothing pointing at anything
{(๐ก, ๐)}
{(๐ก, ๐), (๐ฆ, ๐)}
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐)}
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ฆ)}
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ง)}
Potential execution #1
Potential execution #2
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ฆ)}
โ {(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ง)}
= {(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ฆ), (๐, ๐ง)}
When finishing all potential executions, merge to a main branch using a join operator
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ฆ), (๐, ๐ง), (๐ฆ, ๐), (๐ง, ๐)} ๐ pointed at more than one variable ( ๐ง and ๐ฆ ), so we did not remove the pairs of the form (๐ง, … ) or (๐ฆ, … )
๏ท
Galois Connection
7.6.1
Proof
Let ๐ ๐๐ก
be a group of variable pairs (representing the potential points-to relations) ๐ผ (๐พ(๐ ๐๐ก
)) = ๐ผ({๐|๐ฝ(๐) โ ๐ ๐๐ก
})
We will mark {(๐ฅ, ๐ฆ)|๐(loc(๐ฅ)) = loc(๐ฆ)} = ๐ฝ(๐) = ๐
′ ๐๐ก
. Since ๐ was any state for which it’s abstraction was more accurate than ๐ ๐๐ก
(which means it’s subgroup of variables pointing one to another was contained in ๐ ๐๐ก
), we can have such a ๐ so that ๐
′ ๐๐ก
= ๐ ๐๐ก
.
= ๐ผ({๐|๐
′ ๐๐ก
โ ๐ ๐๐ก ๐
′ ๐๐ก
|๐
′ ๐๐ก
โ ๐ ๐๐ก
} =∪ {๐
′ ๐๐ก
|๐
′ ๐๐ก
⊆ ๐ ๐๐ก
} = ๐ ๐๐ก
⇒ ๐ผ (๐พ(๐ ๐๐ก
)) โ ๐ ๐๐ก
Let ๐ ๐๐ก
= {๐ ๐
} ๐ ๐=1
be a group of concrete states ๐พ (๐ผ(๐ ๐๐ก
)) = ๐พ (๐ผ({๐ ๐
} ๐ ๐=1 ๐
)) = ๐พ ( โ ๐=1 ๐ฝ(๐ ๐
))
We will mark {(๐ฅ, ๐ฆ)|๐ ๐
(loc(๐ฅ)) = loc(๐ฆ)} = ๐ฝ(๐ ๐
) = ๐ ๐๐ก๐
where the same properties hold as in the previous definition. ๐
= ๐พ ( โ ๐=1 ๐ ๐๐ก๐
) = {๐
′
|๐ฝ(๐
′ ๐
) โ โ ๐=1 ๐ ๐๐ก๐
} = {๐
′
|๐ฝ(๐
′ ๐
) ⊆ ∪ ๐=1 ๐ ๐๐ก๐
}
For each ๐ , we can choose ๐ ′ = ๐ ๐
and then ๐ฝ(๐ ′ ) = ๐ ๐๐ก๐ group. From here we can see that def
⊆ ๐
∪ ๐=1 ๐ ๐๐ก๐
and so we have ๐ ๐
in that ๐ ๐๐ก
⊆ {๐
′
|๐ฝ(๐
′ ๐
) ⊆ ∪ ๐=1 ๐ ๐๐ก๐
} = ๐พ (๐ผ(๐ ๐๐ก
)) ⇒ ๐ ๐๐ก
โ ๐พ (๐ผ(๐ ๐๐ก
))
We can easily see that ๐ผ and ๐พ are monotone; ๐ผ is monotone because of its union-like operation, and ๐พ is monotone because of a similar reason. โ
๏ท
Soundness
7.6.2
Proof
We will prove only 2 out of 8 statements, the rest are very similar. What we want to prove is
โฆ๐๐ก๐โง # (๐๐ก) โ ๐ผ({โฆ๐๐ก๐โง๐|๐ ∈ ๐พ(๐ # )})
Or simply
โฆ๐๐ก๐โง # (๐๐ก) ⊇ ๐ผ({โฆ๐๐ก๐โง๐|๐ ∈ ๐พ(๐ #
)})
Let’s begin
7.6.2.1
Constant assignment
โฆ๐ฅ โ ๐โง(๐) = ๐[loc(๐ฅ) → Aโฆ๐โง๐, โฆ๐ฅ โ ๐โง # (๐๐ก) = ๐๐ก − {(๐ฅ, ๐ง)|๐ง ∈ ๐๐๐}, (๐ is a const) ๐ผ({โฆ๐ฅ โ ๐โง๐|๐ ∈ ๐พ(๐๐ก)}) = ๐ผ({๐[๐๐๐(๐ฅ) → ๐]|๐ฝ(๐) โ ๐๐ก}) =โ {๐ฝ(๐[๐๐๐(๐ฅ) → ๐])|๐ฝ(๐) โ ๐๐ก} =
=∪ { ๐ฝ(๐[๐๐๐(๐ฅ) → ๐]) | ๐ฝ(๐) โ ๐๐ก } = ($)
We know that ๐ฝ(๐[๐๐๐(๐ฅ) → ๐]) does not contain any pair of the form (๐ฅ, ๐ง) because ๐ฅ points at a constant ๐ and therefore cannot point at a variable. So the union ($) does not contain any pair of the form (๐ฅ, ๐ง) .
Now, let’s assume (๐ฆ, ๐ง) ∈ ($) and show that (๐ฆ, ๐ง) ∈ ๐๐ก . If (๐ฆ, ๐ง) ∈ ($) , there exists a ๐
′
so that
(๐ฆ, ๐ง) ∈ ๐ฝ(๐ ′ [๐๐๐(๐ฅ) → ๐]) and ๐ฝ(๐ ′ ) โ ๐๐ก
Because ๐ฆ ≠ ๐ฅ we can see that according to ๐ฝ ’s definition
(๐ฆ, ๐ง) ∈ ๐ฝ(๐
′ ) and ๐ฝ(๐
′ ) โ ๐๐ก
(๐ฆ, ๐ง) ∈ ๐๐ก
Now, since (๐ฆ, ๐ง) ∈ ($) , ๐ฆ ≠ ๐ฅ and so (๐ฆ, ๐ง) ∈ ๐๐ก − {(๐ฅ, ๐ค)|๐ค ∈ ๐๐๐} = โฆ๐ฅ โ ๐โง # (๐๐ก)
In total, we got
($) ⊆ โฆ๐ฅ โ ๐โง # (๐๐ก) ๐ผ({โฆ๐ฅ โ ๐โง๐|๐ ∈ ๐พ(๐๐ก)}) โ โฆ๐ฅ โ ๐โง
# (๐๐ก)
7.6.2.2
Assignment into pointer
โฆ๐ฅ โ *๐ฆโง(๐) = ๐ [loc(๐ฅ) → ๐ (๐(loc(๐ฆ)))]
โฆ๐ฅ โ *๐ฆโง # (๐๐ก) = (๐๐ก − {(๐ฅ, ๐ง)|๐ง ∈ ๐๐๐}) ∪ {(๐ฅ, ๐ค)|(๐ฆ, ๐ก), (๐ก, ๐ค) ∈ ๐๐ก} ๐ผ({โฆ๐ฅ โ *๐ฆโง๐|๐ ∈ ๐พ(๐๐ก)}) = ๐ผ ({๐ [๐๐๐(๐ฅ) → ๐ (๐(loc(๐ฆ)))] |๐ฝ(๐) โ ๐๐ก})
=โ {๐ฝ (๐ [๐๐๐(๐ฅ) → ๐ (๐(loc(๐ฆ)))]) |๐ฝ(๐) โ ๐๐ก} =
=∪ { ๐ฝ (๐ [๐๐๐(๐ฅ) → ๐ (๐(loc(๐ฆ)))]) | ๐ฝ(๐) ⊆ ๐๐ก } = ($)
Let (๐ค, ๐ง) be a pair in ($) ๏ there exists a ๐ ′
so that (๐ค, ๐ง) ∈ ๐ฝ (๐ ′ [๐๐๐(๐ฅ) → ๐ ′ (๐ ′ (loc(๐ฆ)))])
Case 1 - ๐ ≠ ๐
Then like previously, we can see that (๐ค, ๐ง) ∈ ๐๐ก − {(๐ฅ, ๐ข)|๐ข ∈ ๐๐๐}
Case 2 - ๐ = ๐ (the pair is now (๐, ๐) )
Then obviously ๐ ′ (๐ ′
(loc(๐ฆ))) = ๐๐๐(๐ง) , so there exists some variable ๐ข so that: ๐
′
(loc(๐ฆ)) = ๐๐๐(๐ข), ๐
′ (๐๐๐(๐ข)) = ๐๐๐(๐ง)
In that case, by ๐ฝ ’s definition, (๐ข, ๐ง) and (๐ฆ, ๐ข) will be in ๐ฝ (๐ ′
[๐๐๐(๐ฅ) → ๐
′ (๐ ′
(loc(๐ฆ)))]) , and since ๐ข, ๐ฆ ≠ ๐ฅ then these relations also exist in ๐ฝ(๐ ′ ) which is included in ๐๐ก .
(๐ฅ, ๐ง) ∈ {(๐ฅ, ๐ค)|(๐ฆ, ๐ข), (๐ข, ๐ค) ∈ ๐๐ก}
(๐ฅ, ๐ง) ∈ (๐๐ก − {(๐ฅ, ๐ง)|๐ง ∈ ๐๐๐}) ∪ {(๐ฅ, ๐ค)|(๐ฆ, ๐ข), (๐ข, ๐ค) ∈ ๐๐ก} = โฆ๐ฅ โ *๐ฆโง # (๐๐ก)
So, from both cases we get ๐ผ({โฆ๐ฅ โ *๐ฆโง๐|๐ ∈ ๐พ(๐๐ก)}) = ($) ⊆ โฆ๐ฅ โ *๐ฆโง
# (๐๐ก)
8
Our analysis of points-to was potentially expensive, since we need to check the neighbor nodes (in the chaotic iteration) and potentially do many iterations in different branches where for each one we need to compute the union of the two sets.
Another approach for the points-to analysis works by ignoring the control-flow and saying that all statements are “neighbors” in the program flow graph. In order to make our analysis sound with such random execution, we never remove any pairs from the set of point-to pairs – we just keep
iterating over all the sentences in the program until we reach a fixed-point (i.e., passing on any sentence now will not make any difference to the group of points-to pairs).
Since we only keep one set of point-to pairs, and we never remove pairs, we can use a union-find data structure, which allows us to add pairs and check for their existence in almost linear time
(amortized inverse Ackerman if we want to be exact).
Sentence t := &a; y := &b; z := &c; if x>0 then p:= &y; else p:= &z;
*p := t;
Abstract state after the current statement
∅
{(๐ก, ๐)}
{(๐ก, ๐), (๐ฆ, ๐)}
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐)}
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ฆ)}
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ฆ), (๐, ๐ง)}
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ฆ), (๐, ๐ง), (๐ฆ, ๐), (๐ง, ๐)}
The final point-to set is
{(๐ก, ๐), (๐ฆ, ๐), (๐ง, ๐), (๐, ๐ฆ), (๐, ๐ง), (๐ฆ, ๐), (๐ง, ๐)}
You can see that if we re-iterate with it, it will not change – so this will be the set of point-to pairs that we will use for all the locations in the program (since we ignored control-flow we can’t say that some are only valid in some place).