Bounded Static Analysis

Everything Everywhere All at Once

Table of Contents
  1. What is Bounded Static Analysis?§1
    1. In Theory§1.1
      1. May and Must Analyses§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
                            2. Getting Started§4

                              In this topic we are going to make 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 are no easy way to do this, because we have to handle a possible infinite set of traces, which by itself is hard to do.

                              The idea behind bounded static analysis is straight forward. Instead of selecting a single initial state and then applying the semantics on that until a trace has been made. This is what we did with dynamic analysis. We start with the set initial states and then apply the semantics to all states at once. Essentially, if dynamic analysis is depth first search, Static Analysis is breath 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.

                              In Theory §1.1

                              The Bounded Static Analysis on a program P can defined as the set of traces of depth n: 𝐁𝐒𝐀Pn. We can get this set using a many-stepping function 𝚜𝚝𝚎𝚙. It takes a set of traces and steps them 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, as applying 𝚜𝚝𝚎𝚙 to the initial state, n times, and taking the union of the results.

                              𝚜𝚝𝚎𝚙P0=IP𝚜𝚝𝚎𝚙Pn=𝚜𝚝𝚎𝚙P(𝚜𝚝𝚎𝚙Pn1)𝚜𝚝𝚎𝚙Pn1
                              𝐁𝐒𝐀Pn=𝚜𝚝𝚎𝚙Pn

                              Now, we can check for assertion errors down to depth n. Let's call this 𝐁𝐀𝐄Pn, by seeing if any trace ends in an assertion error:

                              𝐁𝐀𝐄Pn(τs)𝐁𝐒𝐀Pns=err(‘𝚊𝚜𝚜𝚎𝚛𝚝𝚒𝚘𝚗 𝚎𝚛𝚛𝚘𝚛’)

                              May and Must Analyses §1.2

                              Sadly, it is infeasible, and sometimes impossible, to do computations over all traces at ones. Therefore, when designing an analysis, we either underestimate the set of traces. This is called a must analysis, or overestimate the set of traces which is called a may analysis. A must analysis is named like that, because we know what the program must do, and a may analysis is called that because we know what the program may do.

                              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

                              The may analysis is great if we want to make sure that something newer happens. Essentially, if we want to warn about an assertion error may happen, we need a may analysis. If we want to grantee that an assertion error must happen on an run, then we need a must analysis:

                              𝐁𝐀𝐄𝗆𝗎𝗌𝗍n𝐁𝐀𝐄n¬𝐁𝐀𝐄n¬𝐁𝐀𝐄𝗆𝖺𝗒n

                              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 down into the theory, let's start with a concrete example.

                              Assume that we are working with all the possible integer values that an 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 contain three elements {+,,0}, exactly zero (0), larger than zero (+) and smaller than zero (). This set 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 covert 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["+"] | Literal["-"] | Literal["0"]
                              
                              @dataclass
                              class SignSet:
                                signs : set[Sign]

                              However before we continue this set, we first have to ensure that a computation in 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 the join and the greatest under-approximation of two abstract sets called the meet.

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

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

                              Fasten your seatbelts it's going to be a bumby 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:

                              reflexivea.aaantisymetrica.abbaa=btransitivea.abbcac

                              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 partially ordered sets (L,), with two extra operators and . is the least upper bound ab, meaning that

                              c.acbcabc.

                              The dual is true for that is the greatest lower bound

                              c.cacbcab.

                              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 latices is that they can be drawn using Hasse digrams which gives these nice structures, which looks like a wooden lattice.

                              Here we only draw the imitate 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 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 sets (C,C) and (A,A), where we can abstract information from the concrete domain into the abstract α:CA while preserving the order if going back γ:AC. α 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:

                              cC,aA.cCγ(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 concertize 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:

                              cCγ(α(c))– if we abstract we only increase in sizeα(γ(a))Aa– if we concertize we only decrease in sizeα(γ(α(c)))=α(c)– we don’t keep loosing informationγ(α(γ(a)))=γ(a)– in both directionsγ(a1Aa2)=γ(a1)Cγ(a2)γ preserves joins.α(c1Cc2)=α(c1)Aα(c2)α preserves meets.

                              The two first rules gives us confidence that whatever abstraction we choose only lose information never create it. And the second two rules gives us confidence that we don't keep losing that information.

                              Adjunctions and testing

                              Galoi connections is actually a concrete example on a mathematical construct called adjunction. Adjunctions are one of the most useful and unappreciated mathematical structures in computer science. One example is testing technique for testing 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 are 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 exist a Galoi connection between our concrete integer domain and our abstract sign domain (2,)γα(𝐒𝐢𝐠𝐧,𝐒𝐢𝐠𝐧). Where the abstraction α and concretion γ are defined like this:

                              α(N)={+ | nN,n<0}{ | nN,n>0}{0 | nN,n=0}γ(S)={n | n,n<0,+S}{n | n,n>0,S}{0 | 0S}

                              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.

                              @classmethod
                              def abstract(cls, items : set[int]): 
                                signset = set()
                                if 0 in items:
                                  signset.add("0")
                                ...
                                return cls(signset)

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

                              def __contains__(self, member : int): 
                                if (member == 0 and "0" in self.signs): 
                                    return True
                                ... 

                              You can use the Galoi rules to test your abstraction:

                              X2:Xγ(α(X))

                              If you are using python you fuzz test this property using hypothesis.

                              from hypothesis import given
                              from hypothesis.strategies import integers, sets
                              
                              @given(sets(integers()))
                              def test_valid_abstraction(xs):
                                s = SignSet.abstract(xs) 
                                assert all(x in s for x in xs)

                              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.

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

                              A+2i32B={a+b | aA,bB}{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 newer under-approximate:

                              A+2i32Bγ(α(A)+𝐒𝐢𝐠𝐧α(B))

                              The above equation requires us to create huge sets of values. Instead we can use our Galoi 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 for your sign abstraction.

                              In Python, you can build an Arithmetic class that can handle all the abstract arithmetics. Also if you use hypothesis, you can test that we have done the conversion correctly by, testing the following:

                              @given(sets(integers()), sets(integers()))
                              def test_sign_adds(xs, ys):
                                assert (
                                  SignSet.abstract({x + y for x in xs for y in ys}) 
                                    <= SignSet.abstract(xs) + SignSet.abstract(ys)
                                  )

                              For cases, where we compare like le, we might want to return set of booleans instead.

                              @given(sets(integers()), sets(integers()))
                              def test_sign_adds(xs, ys):
                                  assert (
                                    {x <= y for x in xs for y in ys} 
                                    <= arithmetic.compare("le", SignSet.abstract(xs), SignSet.abstract(ys))
                                    )

                              Bounded Abstract Interpretation §3

                              In practice, Galoi connections allows 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 galoi connection (2𝐓𝐫𝐚𝐜𝐞,)γα(A,A). We can use this new abstract stepping function to define an bounded static analysis of depth n: 𝐁𝐒𝐀An=𝚜𝚝𝚎𝚙An. We define 𝚜𝚝𝚎𝚙n recursively:

                              𝚜𝚝𝚎𝚙A0=α(IP)𝚜𝚝𝚎𝚙An=𝚜𝚝𝚎𝚙A(𝚜𝚝𝚎𝚙An1)A𝚜𝚝𝚎𝚙An1

                              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)).

                              Show that 𝐁𝐒𝐀An a may analysis

                              Using induction over n, show that the following is true: 𝚜𝚝𝚎𝚙(T)Tγ(𝚜𝚝𝚎𝚙A(α(T))Aα(T))𝐁𝐒𝐀nγ(𝐁𝐒𝐀An)

                              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, as it is very hard to talk about the concrete domain, as it is infinite, we can use the galoi connection again to see that we only have to show:

                              α(𝚜𝚝𝚎𝚙(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)={τ|τ| | τT}γ(S)={τ | sS,τ𝐓𝐫𝐚𝐜𝐞,τ|τ|=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 exist 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 | sS,δ(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 exist an assertion error within n steps, then there exist a trace τ𝐁𝐒𝐀n st. τ|τ|=err(‘𝚊𝚜𝚜𝚎𝚛𝚝𝚒𝚘𝚗 𝚎𝚛𝚛𝚘𝚛’) and |τ|n.

                              From our Galoi connection we can see that

                              {τerr(‘𝚊𝚜𝚜𝚎𝚛𝚝𝚒𝚘𝚗 𝚎𝚛𝚛𝚘𝚛’)}𝐁𝐒𝐀nγ(𝐁𝐒𝐀𝐒𝐭𝐚𝐭𝐞n)α({τerr(‘𝚊𝚜𝚜𝚎𝚛𝚝𝚒𝚘𝚗 𝚎𝚛𝚛𝚘𝚛’)}){err(‘𝚊𝚜𝚜𝚎𝚛𝚝𝚒𝚘𝚗 𝚎𝚛𝚛𝚘𝚛’)}𝐁𝐒𝐀𝐒𝐭𝐚𝐭𝐞n

                              Which means that if err(‘𝚊𝚜𝚜𝚎𝚛𝚝𝚒𝚘𝚗 𝚎𝚛𝚛𝚘𝚛’)𝐁𝐒𝐀𝐒𝐭𝐚𝐭𝐞n, then {τerr(‘𝚊𝚜𝚜𝚎𝚛𝚝𝚒𝚘𝚗 𝚎𝚛𝚛𝚘𝚛’)}𝐁𝐒𝐀n. Which is exactly the grantee we are looking for in a may analysis.

                              The Per-Instruction Abstraction §3.2

                              In our next abstraction, we want to take advantage of that 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 talk about how to handle the method 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 𝐏𝐜 were one mapping is less than another if all states are smaller than the other and 𝐏𝐜 is pointwise of states.

                              (2𝐒𝐭𝐚𝐭𝐞,)γα(𝐏𝐜,𝐏𝐜)

                              Convince yourself this is a Galoi 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, that we build 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 stack of possible values:

                              𝐏𝐯=ι((2𝐕σ))×(2𝐕σ)*{ok,err(‘.’)}

                              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 point wise comparing the set of variables, and a lattice by point-wise joining and meeting the sets of variables. We can also see that 𝐏𝐜γα𝐏𝐯) is a galoi connection.

                              The problem with this abstraction, and the reason this choice is hard, is that 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, is an element that joins with all others, Err represent the error state and Ok represent the Ok state.

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

                              @dataclass
                              class PerVarFrame[AV]:
                                locals: dict[int, V]
                                stack: Stack[AV]

                              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*{ok,err(‘.’)}

                              If we can see A is an abstraction of 2𝐕σ, then we also get a Galoi 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 Galoi connections is that they compose. This mean we can create one long chain:

                              (2𝐓𝐫𝐚𝐜𝐞,)γα(2𝐒𝐭𝐚𝐭𝐞,)γα(𝐏𝐜,𝐏𝐜)γα(𝐏𝐯,𝐏𝐯)γα(𝐏𝐯[𝐒𝐢𝐠𝐧],𝐏𝐯[𝐒𝐢𝐠𝐧])

                              Getting Started §4

                              To get started with the building your first static analysis, first refactor your interpreter to handle a more abstract state.

                              First, don't try to do everything at once! Start with a single frame.

                              Then, we need to realize that we can no longer return just one state. Eg., in the case where we are handling ifz we might need to return two states one where we jump and one where we don't.

                              def step(state : AState) -> Iterable[AState | str]
                                ...

                              Secondly, do all the steps at once:

                              def many_step(state : dict[Pc, AState | str]) 
                                -> dict[Pc, AState | str]:
                                new_state = dict(state)
                                for k, v in state.items():
                                    for s in step(v):
                                      new_state[s.pc] |= s
                                return new_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 analysing code?

                              2. What is the advantage of Galoi connections?

                              3. Which cases are the Sign abstraction not able to catch?

                              4. At which cases do your static analysis outperform your dynamic?

                              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.