Bounded Static Analysis

Everything Everywhere All at Once

Table of Contents
  1. What is Bounded Static Analysis?Β§1
    1. Goal and NotationΒ§1.1
      1. The Many Step FunctionΒ§1.2
      2. AbstractionsΒ§2
        1. The Sign AnalysisΒ§2.1
          1. Partially Ordered Sets (Posets)Β§2.2
            1. LatticesΒ§2.3
              1. Hasse diagrams.Β§2.3.1
                1. Back to the Sign AbstractionΒ§2.3.2
                2. Galois ConnectionΒ§2.4
                  1. Back to the Sign AnalysisΒ§2.4.1
                  2. Abstract OperationsΒ§2.5
                  3. Bounded Abstract InterpretationΒ§3
                    1. The State AbstractionΒ§3.1
                      1. The Per-Instruction AbstractionΒ§3.2
                        1. The Per-Variable AbstractionΒ§3.3
                          1. Variable AbstractionsΒ§3.4
                            1. Abstractions All The Way Down.Β§3.5

                            In this topic, we are going to perform our first static analysis. It is based on abstract interpretation.

                            What is Bounded Static Analysis? Β§1

                            In bounded static analysis, we want to talk about all the behaviors of the program up to some depth. There is no easy way to do this, because we have to handle a potentially infinite set of traces, which is hard enough on its own.

                            The idea behind bounded static analysis is straightforward: instead of selecting a single initial state and then applying the semantics to that until a trace has been made, we do something different. This is what we did with dynamic analysis. We start with the set of initial states and then apply the semantics to all states at once. Essentially, if dynamic analysis is depth-first search, static analysis is breadth-first. While we have the downside of having to work with infinite sets of traces, we can be smart about it and use abstractions to represent the set using a finite number of traces.

                            Goal and Notation Β§1.1

                            The goal of any static analysis is to soundly predict properties that must happen, e.g ., a property which is true for all traces. For bounded static analyses, we make this problem a little easier by only considering traces up to length n.

                            We write that the property Q was found true in program p for all traces smaller than n, like so p,nβŠ’ππ’π€π—†π—Žπ—Œπ—Q. A sound bounded static analysis will imply that any property found is also be true in the program p:

                            p,nβŠ’ππ’π€π—†π—Žπ—Œπ—Q⟹p,nβŠ¨π—†π—Žπ—Œπ—Q

                            In practice, we often solve a negated may problem. Instead of assuring that a property holds for all traces of size n, we check if we can disprove that there exist a trace of size n in which the property does not hold:

                            Β¬(p,nβŠ’ππ’π€Β¬π—†π–Ίπ—’Β¬Q)≑p,nβŠ’ππ’π€π—†π—Žπ—Œπ—Q

                            We use the βŠ’ππ’π€Β¬π—†π–Ίπ—’ notation to indicate that our may analysis should be complete:

                            p,nβŠ¨π—†π–Ίπ—’Q⟹p,nβŠ’ππ’π€Β¬π—†π–Ίπ—’Β¬Q

                            Assertion Error

                            We can check if a trace contains an assertion error by inspecting the last element:

                            𝚊𝚎(Ο„)≑τ|Ο„|βˆ’1=err(β€˜πšŠπšœπšœπšŽπš›πšπš’πš˜πš— πšŽπš›πš›πš˜πš›β€™)

                            In a static analysis, we would like to guarantee that an assertion error does not happen:

                            p,nβŠ¨π—†π—Žπ—Œπ—Β¬πšŠπšŽβ‰‘βˆ€Ο„βˆˆSemn(p).¬𝚊𝚎(Ο„)

                            We can then use a not-may analysis for assertion errors 𝚊𝚎 down to depth n. Essentially, if we cannot find a trace which emits an assertion error, we conclude that the program must not contain a trace which emits an assertion error.

                            Β¬(p,nβŠ’ππ’π€Β¬π—†π–Ίπ—’πšŠπšŽ)≑p,nβŠ’ππ’π€π—†π—Žπ—Œπ—Β¬πšŠπšŽ

                            The Many Step Function Β§1.2

                            Bounded Static Analysis on a program p can be defined as the set of traces of depth n. We can get this set using a multi-step function Ξ”. It takes a set of traces T and advances them by one step.

                            Ξ”p(T)={Ο„β€²sΒ |Β s∈Σ,Ο„β€²βˆˆT,Ξ΄p(Ο„β€²|Ο„β€²|,s)}

                            In the definition of Ξ”, Ξ΄ is the transition relation defined by the single step semantics, and Ο„β€²s means appending s to Ο„β€². Since we are always talking about the same program p, we'll sometimes omit it.

                            Now, we can define the set of traces of up to length n, by applying Ξ” to the initial state, n times, and taking the union of the results.

                            Ξ”p0=IpΞ”pn=Ξ”p(Ξ”pnβˆ’1)βˆͺΞ”pnβˆ’1

                            Sadly, it is infeasible, and sometimes impossible, to do computations over all traces at once. Therefore, when designing an analysis, we either underestimate the set of traces or overestimate the set of traces.

                            In a may analysis we overestimate every step, and in the must analysis we underestimate every step:

                            Ξ”π—†π—Žπ—Œπ—(T)βŠ†Ξ”(T)βŠ†Ξ”π—†π–Ίπ—’(T)

                            Naturally, we'll see that

                            Ξ”π—†π—Žπ—Œπ—,nβŠ†Ξ”nβŠ†Ξ”π—†π–Ίπ—’,n

                            From this we get two complete analyses, a not-may analysis:

                            p,nβŠ’ππ’π€Β¬π—†π–Ίπ—’Qβ‰‘βˆƒΟ„βˆˆΞ”π—†π–Ίπ—’,n.Q(Ο„)

                            and a not-must analysis:

                            p,nβŠ’ππ’π€Β¬π—†π—Žπ—Œπ—Qβ‰‘βˆ€Ο„βˆˆΞ”π—†π—Žπ—Œπ—,n.Q(Ο„)

                            As we saw in the previous section, the best approach is to look for negated properties and use the not-may analysis.

                            However, it is unclear why allowing us to approximate the result will allow us to run the analyses at all. We need abstractions!

                            Abstractions Β§2

                            Instead of working with sets of traces, we have to work with finite structures instead. We call these abstractions.

                            The Sign Analysis Β§2.1

                            Before we go into the theory, let's start with a concrete example. Assume that we are working with all the possible integer values that a variable has at some point in the program. It would be much easier to simply keep track of whether the set contains positive, negative, or zero values.

                            We can represent this as a set that only contains three elements {+,βˆ’,0}, exactly zero (0), larger than zero (+) and smaller than zero (βˆ’). This set is of size 3!, which is much easier to work with than the set of size 232!. In the following sections, we are going to refer to this set as 𝐒𝐒𝐠𝐧.

                            𝐒𝐒𝐠𝐧={+,0,βˆ’}

                            We call 𝐒𝐒𝐠𝐧 an abstraction, as we can convert the concrete domain of integers into the abstract domain of 𝐒𝐒𝐠𝐧. We call this conversion the abstraction Ξ±:

                            Ξ±({1,2,3})={+}Ξ±({0})={0}Ξ±({βˆ’1,3})={+,βˆ’}Ξ±({βˆ’1,0,3})={0,+,βˆ’}

                            Make the Sign Abstraction

                            Implement the SignSet as an abstract set in the language of your choosing.

                            In Python, you can start with this:

                            from dataclasses import dataclass
                            from typing import TypeAlias, Literal
                            
                            Sign : TypeAlias = Literal[1] | Literal[-1] | Literal[0]
                            
                            @dataclass
                            class SignSet:
                                signs : frozenset[Sign]
                            
                                ...

                            Hint (Hover to see)

                            The solution is in solutions/static/src/abstractions.py.

                            However, before we continue with this set, we first have to ensure that a computation in an abstract domain does not invalidate our over-approximation.

                            In the following sections, we'll see that:

                            • Our abstract domains can be ordered.

                            • We can get the least over-approximation of two abstract sets, called a join, and the greatest under-approximation of two abstract sets, called a meet.

                            • Then, we have a formal definition of a correct over-approximation or under-approximation called a Galois connection.

                            • And finally, we'll cover monotone functions, which are functions that preserve order.

                            Fasten your seatbelts it's going to be a bumpy night!

                            Partially Ordered Sets (Posets) Β§2.2

                            A partially ordered set or poset is a tuple (L,βŠ‘) set of elements L and an ordering βŠ‘, that uphold:

                            reflexiveβˆ€a∈L.aβŠ‘aantisymmetricβˆ€a,b∈L.aβŠ‘b∧bβŠ‘a⟹a=btransitiveβˆ€a,b,c∈L.aβŠ‘b∧bβŠ‘c⟹aβŠ‘c

                            Common partially ordered sets are the integers (β„€,≀) (also in the other direction (β„€,β‰₯)), the booleans ({𝚝𝚝,𝚏𝚏},β‡’), and the set of 𝐒𝐒𝐠𝐧′s (2𝐒𝐒𝐠𝐧,βŠ†).

                            Check the rules

                            Think about if the rules apply to the posets above.

                            Make Sign a partially ordered set

                            Make your 𝐒𝐒𝐠𝐧 abstraction into a partially ordered set by implementing βŠ‘ (<=) in the language of your choice.

                            Lattices Β§2.3

                            A lattice is a partially ordered set (L,βŠ‘), with two extra operators βŠ” and βŠ“. βŠ” is the least upper bound aβŠ”b, meaning that

                            βˆ€c.aβŠ‘c∧bβŠ‘c⟹aβŠ”bβŠ‘c.

                            The dual is true for βŠ“, which is the greatest lower bound

                            βˆ€c.cβŠ‘a∧cβŠ‘b⟹cβŠ‘aβŠ“b.

                            Furthermore, this implies that there exist a least bound βŠ₯=β¨…L and a greatest bound ⊀=⨆L, from which we have the following identities: βŠ€βŠ“a=a=aβŠ”βŠ₯, and βŠ€βŠ”a=⊀ and aβŠ“βŠ₯=βŠ₯.

                            Hasse diagrams. Β§2.3.1

                            Hasse diagram phi Ø x->phi x {+} y->phi y {0} z->phi z {-} xy->x xy->y xy {+, 0} xz->x xz->z xz {+, -} yz {0, -} yz->y yz->z xyz {+,0,-} xyz->yz xyz->xz xyz->xy

                            A Hasse diagram over the poset (2𝐒𝐒𝐠𝐧,βŠ†). By I, KSmrq, CC BY-SA 3.0 (with edits)

                            The reason why they are called lattices is that they can be drawn using Hasse diagrams, which gives these nice structures that look like a wooden lattice.

                            Here we only draw the immediate next elements in the order. So while {+}βŠ†{+,0,βˆ’} we do not draw that edge because {+,0} and {+,βˆ’} are in the way.

                            Rate your favourite animals

                            Create a Hasse diagram over your favorite animals. The order is (𝐀𝐧𝐒𝐦,β€˜πš’πšœ πš πš˜πš›πšœπšŽ πšπš‘πšŠπš—β€™). Where

                            𝐀𝐧𝐒𝐦={Cat,Dog,Ant,Worm,Spider,Horse,Rabbit}

                            While objectively (Catβ€˜πš’πšœ πš πš˜πš›πšœπšŽ πšπš‘πšŠπš—β€™Dog), some of the other animals like Ant and Worm are no worse than each other.

                            Is your order a lattice: Does every pair of animals have a meet and a join?

                            Back to the Sign Abstraction Β§2.3.2

                            In our Sign abstraction, we use set inclusion as our order βŠ‘π’π’π π§=βŠ† and use the intersection and union of the underlying set as the meet (βŠ“) and join (βŠ”) of our lattice. βŠ₯=βˆ… and ⊀={+,βˆ’,0}.

                            Make the Sign a lattice

                            To make your 𝐒𝐒𝐠𝐧 abstraction into a lattice, you need to also implement the meet βŠ“ and join βŠ” operators.

                            In Python, you can implement __and__ and __or__ to get the meet operator a & b, and the join operator a | b.

                            Galois Connection Β§2.4

                            (A Galois Connection is a connection between two ordered sets, with a concretion Ξ³ and an abstraction Ξ± function.)

                            A Galois connection is a relationship between two ordered and ordered sets (C,βŠ‘C) and (A,βŠ‘A), where we can abstract information from the concrete domain into the abstract Ξ±:Cβ†’A while preserving the order if going back Ξ³:Aβ†’C. Ξ± explains how to abstract a value, and Ξ³ explains what the abstraction means.

                            Because the abstraction might be lossy, it is not the case that if we abstract a value we get back the same concrete value Ξ³(Ξ±(c))β‰ c. Instead, a Galois connection satisfies the following rule:

                            βˆ€c∈C,a∈A.cβŠ‘CΞ³(a)⇔α(c)βŠ‘Aa

                            It states that if we have a concrete value c, and abstract it Ξ±(c), then any value larger than this abstraction a will concretize to a value Ξ³(a) that is larger than the original value. At first this might not seem useful, but if we satisfy this rule we get the following laws for free:

                            cβŠ‘CΞ³(Ξ±(c))– if we abstract we only increase in sizeΞ±(Ξ³(a))βŠ‘Aa– if we concretize we only decrease in sizeΞ±(Ξ³(Ξ±(c)))=Ξ±(c)– we don’t keep losing informationΞ³(Ξ±(Ξ³(a)))=Ξ³(a)– in both directionsΞ³(a1βŠ”Aa2)=Ξ³(a1)βŠ”CΞ³(a2)– Ξ³ preserves joins.Ξ±(c1βŠ“Cc2)=Ξ±(c1)βŠ“AΞ±(c2)– Ξ± preserves meets.

                            The first two rules give us confidence that whatever abstraction we choose only loses information and never creates it. And the second two rules give us confidence that we don't keep losing that information.

                            Adjunctions and testing

                            Galois connections are actually a concrete example of a mathematical construct called adjunction. Adjunctions are one of the most useful and unappreciated mathematical structures in computer science. One example is a testing technique for data readers.

                            While in most cases we would expect anything we pretty-print to be parsable to the same value. That is not always the case, as line numbers might differ or some data is lost in the translation. It should, however, always hold that

                            # Most of the time
                            parse(pretty(a)) == a 
                            
                            # Always
                            pretty(parse(pretty(a))) == pretty(a)
                            
                            # Bonus for all strings s:
                            parse(pretty(parse(s))) == parse(s)

                            Back to the Sign Analysis Β§2.4.1

                            We can see that there exists a Galois connection between our concrete integer domain and our abstract sign domain (2β„€,βŠ†)↔γα(𝐒𝐒𝐠𝐧,βŠ‘π’π’π π§), where the abstraction Ξ± $ and concretion Ξ³ $ are defined like this:

                            Ξ±(N)={+Β |Β n∈N,n<0}βˆͺ{βˆ’Β |Β n∈N,n>0}βˆͺ{0Β |Β n∈N,n=0}Ξ³(S)={nΒ |Β nβˆˆβ„€,n<0,+∈S}βˆͺ{nΒ |Β nβˆˆβ„€,n>0,βˆ’βˆˆS}βˆͺ{0Β |Β 0∈S}

                            We can also see that we satisfy the rules; let's pick a concrete value {0,1}. If we convert that to an abstract value we get: Ξ±({0,1})={0,+}.

                            Since Ξ±({0,1})βŠ‘{0,+}βŠ‘{0,+,βˆ’}, and since Ξ³({0,+})=β„€0,+ and Ξ³({0,+,βˆ’})=β„€0,+,βˆ’, then we get that {0,1}βŠ†β„€0,+βŠ†β„€.

                            Build the Sign abstraction

                            You need an abstract function, which takes a set and turns it into your abstract domain.

                            def to_sign(value: jvms.StackValue) -> Sign:
                                match value:
                                    case jvms.StackInt(v):
                                        return (v > 0) - (v < 0)
                                    case a:
                                        raise NotImplementedError(f"Unsupported value {value!r}")
                            
                            class SignSet:
                              ...
                              @classmethod
                              def abstract(cls, items : Iterable[jvms.StackValue]): 
                                signs = set()
                                for value in values:
                                    signs.add(to_sign(value))
                                return cls(frozenset(signs))

                            Then because we are using possible infinite sets, instead of generating that set, we can just implement the contains method:

                            def __contains__(self, value : jvms.StackValue): 
                                return to_sign(value) in self.signs

                            You can use the Galois rules to test your abstraction:

                            βˆ€XβŠ‚2β„€:XβŠ†Ξ³(Ξ±(X))

                            If you are using Python, you can fuzz test this property using hypothesis. A working example is provided in solutions/static/tests/test_abstractions.py. You can run the tests using uv run pytest.

                            Abstract Operations Β§2.5

                            Finally, we have reached the fun part. We can now define operations in the abstract domain that mimic the operations in the concrete domain.

                            Because our concrete domain is the set of integers, we can define +2i3pi32, as the operation over the product:

                            A+2i32B={a+b | a∈A,b∈B}{1,3}+2i32{0,10}={1,3,11,13}

                            Now we can also define the plus abstract domain:

                            {+}+𝐒𝐒𝐠𝐧{+}={+}{+}+𝐒𝐒𝐠𝐧{βˆ’}={βˆ’,+,0}{+}+𝐒𝐒𝐠𝐧{0}={+}{0}+𝐒𝐒𝐠𝐧{+}={+}{0}+𝐒𝐒𝐠𝐧{βˆ’}={βˆ’}{0}+𝐒𝐒𝐠𝐧{0}={0}{βˆ’}+𝐒𝐒𝐠𝐧{+}={βˆ’,+,0}{βˆ’}+𝐒𝐒𝐠𝐧{βˆ’}={βˆ’}{βˆ’}+𝐒𝐒𝐠𝐧{0}={βˆ’}…

                            And many more.

                            But, how do we know if we have implemented our abstract operation correctly? Since we want to over-approximate our operations, we know that computing in our abstract domain should never under-approximate:

                            A+2i32BβŠ†Ξ³(Ξ±(A)+𝐒𝐒𝐠𝐧α(B))

                            The above equation requires us to create huge sets of values. Instead, we can use our Galois connection to rewrite the equation to see that we only have to show the following:

                            Ξ±(A+2i32B)βŠ†Ξ±(A)+𝐒𝐒𝐠𝐧α(B)

                            Essentially, abstracting the result of an operation should be smaller than abstracting and doing the operation in the abstract domain.

                            Implement Abstract Arithmetic

                            Implement arithmetic and comparators for your sign abstraction.

                            Check out the test_signset_arithmetic and test_signset_compare functions in, and expand the arithmetic and compare methods in the SignSet class.

                            Bounded Abstract Interpretation Β§3

                            In practice, Galois connections allow us to map our infinite domain of may or must analyses into a more manageable abstract domain, while giving us guarantees that we are still correctly over - or under-estimating.

                            Let's assume we are building a may analysis. Since we want to over-approximate our Ξ” function, we therefore introduce an abstract domain (A,βŠ‘A) that has an abstract stepping function Ξ”A. We assume the Galois connection (2π“π«πšπœπž,βŠ†)↔γα(A,βŠ‘A). We can use this new abstract stepping function to define a bounded static analysis of depth n. We define Ξ”n $ recursively:

                            Ξ”A0=Ξ±(IP)Ξ”An=Ξ”A(Ξ”Anβˆ’1)βŠ”AΞ”Anβˆ’1

                            We can prove by induction over n that stepping in the abstract domain is in fact a may analysis Ξ”nβŠ†Ξ³(Ξ”An), given that

                            Ξ”(T)βˆͺTβŠ†Ξ³(Ξ”A(Ξ±(T))βŠ”AΞ±(T)).

                            We can also show that Ξ”(T)βŠ†Ξ³(Ξ”A(Ξ±(T)) is a stronger statement, and is sufficient to prove the above, by seeing that Ξ”(T)βˆͺTβŠ†Ξ³(Ξ”A(Ξ±(T))βŠ”AΞ±(T)). From TβŠ†Ξ³(Ξ±(T)). And using that gamma preserves meets, we get Ξ”(T)βˆͺTβŠ†Ξ³(Ξ”A(Ξ±(T))βˆͺΞ³(Ξ±(T))=Ξ³(Ξ”A(Ξ±(T)βŠ”AΞ±(T))

                            Finally, since the concrete domain is infinite and therefore difficult to discuss directly, we can use the Galois connection again to show that we only have to prove:

                            Ξ±(Ξ”(T))βŠ‘AΞ”A(Ξ±(T))

                            The State Abstraction Β§3.1

                            The first, and most useful abstraction, is the state abstraction. Here we throw away everything from the trace except the final state of the trace:

                            (2π“π«πšπœπž,βŠ†)↔γα(2π’π­πšπ­πž,βŠ†)

                            We can define the abstraction as the set of end states of the traces and the concretion as the set of traces that end in one of the states.

                            Ξ±(T)={Ο„|Ο„|βˆ’1Β |Β Ο„βˆˆT}Ξ³(S)={τ |Β s∈S,Ο„βˆˆπ“π«πšπœπž,Ο„|Ο„|βˆ’1=s}

                            This is a useful abstraction for us, because we do not need to know where the trace came from to figure out if an assertion error exists in the program, only that a trace ends in an assertion error.

                            We can now define Ξ”π’π­πšπ­πž as only stepping the final state of each trace:

                            Ξ”π’π­πšπ­πž(S)={sβ€²Β |Β s∈S,Ξ΄(s,sβ€²)}

                            Check that Ξ”π’π­πšπ­πž is an abstract operation of Ξ”

                            Ξ±(Ξ”(T))βŠ†Ξ”π’π­πšπ­πž(Ξ±(T))

                            Now let Ξ”π’π­πšπ­πžn=Ξ”π’π­πšπ­πžn(Ξ±(IP)). It is now clear by induction on n that Ξ”nβŠ†Ξ³(Ξ”π’π­πšπ­πžn). So if there exists an assertion error within n $ steps, then there exists a trace Ο„βˆˆΞ”n such that Ο„|Ο„|=err(β€˜πšŠπšœπšœπšŽπš›πšπš’πš˜πš— πšŽπš›πš›πš˜πš›β€™) and |Ο„|≀n.

                            From our Galois connection, we can see that

                            {Ο„err(β€˜πšŠπšœπšœπšŽπš›πšπš’πš˜πš— πšŽπš›πš›πš˜πš›β€™)}βŠ†Ξ”nβŠ†Ξ³(Ξ”π’π­πšπ­πžn)⟹α({Ο„err(β€˜πšŠπšœπšœπšŽπš›πšπš’πš˜πš— πšŽπš›πš›πš˜πš›β€™)})βŠ†{err(β€˜πšŠπšœπšœπšŽπš›πšπš’πš˜πš— πšŽπš›πš›πš˜πš›β€™)}βŠ†Ξ”π’π­πšπ­πžn

                            Which means that if err(β€˜πšŠπšœπšœπšŽπš›πšπš’πš˜πš— πšŽπš›πš›πš˜πš›β€™)βˆ‰Ξ”π’π­πšπ­πžn, then {Ο„err(β€˜πšŠπšœπšœπšŽπš›πšπš’πš˜πš— πšŽπš›πš›πš˜πš›β€™)}βŠˆΞ”n. Which is exactly the guarantee we are looking for in a may analysis.

                            The Per-Instruction Abstraction Β§3.2

                            In our next abstraction, we want to take advantage of the fact that we execute the same instruction in states with the same program counter.

                            Let's focus on a JVM without a method stack, which means that every state is βŸ¨Οƒ,Ξ»,ι⟩. We'll get back to discussing how to handle the method/call stack in later sections. We can abstract this by collecting the states per ΞΉ. Let 𝐏𝐜=ΞΉβ†’2π’π­πšπ­πž be a mapping from program counters to sets of states. 𝐏𝐜 is a lattice with partial order βŠ‘ππœ where one mapping is less than another if all corresponding states are smaller and βŠ”ππœ is the pointwise βˆͺ $ of states.

                            (2π’π­πšπ­πž,βŠ†)↔γα(𝐏𝐜,βŠ‘ππœ)

                            Convince yourself this is a Galois connection

                            Now we can write a stepping function that can step all states with the same instruction at once Δι, and then merge the result into the other states.

                            Ξ”ππœ(C)=β¨†ππœ{ι′↦{βŸ¨Οƒβ€²,Ξ»β€²,ΞΉβ€²βŸ©}Β |Β (ι↦S)∈C,Sβ€²=Δι(S),βŸ¨Οƒβ€²,Ξ»β€²,ΞΉβ€²βŸ©βˆˆSβ€²}

                            The Per-Variable Abstraction Β§3.3

                            We are now faced with our first hard choice.

                            We would love to use the Sign abstraction, which we built in the previous section. One way of doing that is compressing the state, so that instead of having a set of states, we distribute the content of the locals and stack. So instead of having a set of states, we have a pair of abstract locals and abstract stack. The abstract locals is not a mapping from naturals to a powerset of possible values, and the abstract stack is a stack of possible values:

                            𝐏𝐯=ΞΉβ†’(ℕ↦(2𝐕σ))Γ—(2𝐕σ)*

                            Since the stack and the locals are typed at each instruction, these are actually sets of integers, which we know how to abstract using the sign abstraction. Furthermore 𝐏𝐯 is also a partially ordered set, by pointwise comparing the set of variables, and a lattice by pointwise joining and meeting the sets of variables. We can also see that ππœβ†”Ξ³Ξ±ππ―) is a Galois connection.

                            The problem with this abstraction, and the reason this choice is hard, is that it is the first that severely over-approximates our problem. More about this next time.

                            Define the Abstract State

                            Define the Abstract (per variable) Frame in your program analysis; remember to define the ordering as well as the meet and join operations. It might be useful to define three pseudo elements: Bot, which is an element that joins with all others; Err, which represents the error state; and Ok, which represents the Ok state.

                            In Python, we could define an abstract frame like so:

                            @dataclass
                            class State:
                              locals: tuple[SignSet, ...]
                              stack: tuple[SignSet, ...]

                            Remember to define the abstract, __le__, __and__, and __or__ methods.

                            Variable Abstractions Β§3.4

                            Finally, we are at a point where we can use our Sign Abstraction, by using it to abstract the sets of variables.

                            𝐏𝐯[A]=ΞΉβ†’(ℕ↦A)Γ—A*

                            If we can see A is an abstraction of 2𝐕σ, then we also get a Galois connection from 𝐏𝐯 to Pv[A].

                            (2𝐕σ,βŠ†)↔γα(A,βŠ‘A)⟹(𝐏𝐯,βŠ‘ππ―)↔γα(𝐏𝐯[A],βŠ‘ππ―[A])

                            Specifically, we get:

                            (𝐏𝐯,βŠ‘ππ―)↔γα(𝐏𝐯[𝐒𝐒𝐠𝐧],βŠ‘ππ―[𝐒𝐒𝐠𝐧])

                            Experiment with different Abstractions

                            You have been introduced to the Sign Abstraction, but next time we'll look at more different abstractions. If you have time, try to come up with your own.

                            Abstractions All The Way Down. Β§3.5

                            One great thing about Galois connections is that they compose. This means we can create one long chain:

                            (2π“π«πšπœπž,βŠ†)↔γα(2π’π­πšπ­πž,βŠ†)↔γα(𝐏𝐜,βŠ‘ππœ)↔γα(𝐏𝐯,βŠ‘ππ―)↔γα(𝐏𝐯[𝐒𝐒𝐠𝐧],βŠ‘ππ―[𝐒𝐒𝐠𝐧])

                            Getting Started

                            To get started with building your first static analysis, first complete the implementation of the SignSet analysis in the abstractions.py file, with the help of the test_abstractions.py tests.

                            Then go on to add steps to the manystep function in src/static.py using your interpreter as a guide.

                            After this, we can get creative by abstracting more of the state.

                            Until next time!

                            Until next time, write the best analysis you can and upload the results to Autolab, and ponder the following:

                            1. Why should we use abstractions when analyzing code?

                            2. What is the advantage of Galois connections?

                            3. Which cases can the Sign abstraction not catch?

                            Bibliography

                            1. Cousot, Patrick; Cousot, Radhia (1977). Abstract interpretation. doi:10.1145/512950.512973 link

                            2. Cousot, Patrick; Cousot, Radhia (1979). Systematic design of program analysis frameworks. doi:pdf/10.1145/567752.567778 link

                            3. Nielson, Hanne Riis; Nielson, Flemming (2007). Semantics with Applications.