Combinator-Based Fixpoint Algorithms for Big-Step Abstract Interpreters

Big-step abstract interpreters are an approach to build static analyzers based on big-step interpretation. While big-step interpretation provides a number of benefits for the definition of an analysis, it also requires particularly complicated fixpoint algorithms because the analysis definition is a recursive function whose termination is uncertain. This is in contrast to other analysis approaches, such as small-step reduction, abstract machines, or graph reachability, where the analysis essentially forms a finite transition system between widened analysis states. We show how to systematically develop sophisticated fixpoint algorithms for big-step abstract interpreters and how to ensure their soundness. Our approach is based on small and reusable fixpoint combinators that can be composed to yield fixpoint algorithms. For example, these combinators describe the order in which the program is analyzed, how deep recursive functions are unfolded and loops unrolled, or they record auxiliary data such as a (context-sensitive) call graph. Importantly, each combinator can be developed separately, reused across analyses, and can be verified sound independently. Consequently, analysis developers can freely compose combinators to obtain sound fixpoint algorithms that work best for their use case. We provide a formal metatheory that guarantees a fixpoint algorithm is sound if its composed from sound combinators only. We experimentally validate our combinator-based approach by describing sophisticated fixpoint algorithms for analyses of Stratego, Scheme, and WebAssembly.


INTRODUCTION
Abstract interpretation [Cousot and Cousot 1977] is a methodology for defining sound static analyses.While in the past, many static analyses have been described as abstract interpreters in small-step style [Darais et al. 2015;Horn and Might 2010;Might and Shivers 2006a,b;Schmidt 1996;Sergey et al. 2013], more recently big-step abstract interpreters have been investigated more thoroughly [Bodin et al. 2019;Darais et al. 2017;Keidel and Erdweg 2019;Keidel et al. 2018;Wei et al. 2019].Such big-step abstract interpreters can be simply described as recursive functions in any meta-language; we use Haskell as a meta-language throughout this paper.
Big-step abstract interpreters (sometimes called definitional abstract interpreters) look like the corresponding concrete interpreter, except they compute with abstract data.For example, consider the big-step abstract interpreter in Listing 1 that approximates values using intervals.However, it takes effort to develop and maintain many different fixpoint algorithms and their soundness proofs.Therefore many proofs become outdated over time and they become ineffective at guaranteeing soundness of the analysis.Fine-tuning existing fixpoint algorithms.Fixpoint algorithms require continuous fine-tuning to yield satisfactory performance and precision.In particular, no fixpoint algorithm works best in all cases and configurability is key.For example, consider the continuous changes to the fixpoint algorithms in the OPAL [Helm et al. 2020] and TAJS [Jensen et al. 2009] analysis frameworks depicted in Figure 1.Unfortunately, fine-tuning a single monolithic fixpoint algorithm has two problems.First, tuning it for one analysis may lead to regressions for other analyses that use the same algorithm.Second, every change to the fixpoint algorithm can introduce a soundness bug, yet reestablishing the soundness proof for every single change is infeasible in practice.These two problems cause framework developers either to avoid fine-tuning their fixpoint algorithms or to avoid proving them sound rigorously.
To support such scenarios, we propose a novel approach for the implementation of big-step fixpoint algorithms based on reusable and separately verifiable fixpoint combinators.Note that we mean combinators in the sense of parser combinators, where complex functions can be constructed by composing simpler functions. 5For example, fixpoint combinators describe the order in which the program statements are analyzed, how deep recursive functions are unfolded or loops are unrolled, or they record auxiliary data such as a control-flow graph.A complete fixpoint algorithm can then be composed by choosing appropriate fixpoint combinators (each starting with ): filter isFunBody ( unfold 3 stackWiden • innermost ) This fixpoint algorithm only applies to function bodies ( filter isFunBody) and unfolds the first 3 recursive function calls ( unfold ) before it applies a widening operator on the stack ( stackWiden ).In case of nested recursive function calls, the fixpoint algorithm stabilizes the analysis result of the innermost calls first ( innermost ).
This modular description allows analysis developers to specialize and fine-tune fixpoint algorithms by reconfiguring individual combinators or adding specialized ones.For example, we can extend the fixpoint algorithm from above to record the call graph ( CFG ) and to handle while-loops: This fixpoint algorithm seamlessly interleaves the intra-procedural analysis of loops with the inter-procedural analysis of recursive function calls.Both of these aspects can be individually changed and fine-tuned by adding, replacing, and reordering fixpoint combinators.And of course we can use standard function abstraction to make parts of the fixpoint algorithm reusable.
We also modularize the soundness proofs of big-step fixpoint algorithms by developing a formal theory for fixpoint combinators.The modularization simplifies the effort and complexity of these soundness proofs and makes them composable.In particular, we prove that a modular fixpoint algorithm is sound if all of its combinators are sound.This not only simplifies the initial soundness proof for a fixpoint algorithm but also makes it easier to reestablish soundness after a change.
We demonstrate that our approach is feasible and useful by implementing it in Haskell as part of the Sturdy framework [Keidel and Erdweg 2019;Keidel et al. 2018].We developed 12 fixpoint combinators and composed them to obtain fixpoint algorithms for 3 analyses of 3 different languages: WebAssembly, Stratego, and Scheme.We use these case studies to assess the language and analysisindependence, the precision, and the performance of the fixpoint algorithms.We find that the initial fixpoint algorithms perform poorly, but they can be easily specialized to the analysis without changing the implementation of any of the fixpoint combinators.We conclude that configurable fixpoint algorithms are necessary to allow analysis developers to fine-tune their analyses.
In summary, we make the following contributions: • We combine prior work on big-step fixpoint algorithms [Schmidt 1995] and chaotic iteration [Bourdoncle 1993] to develop a novel big-step fixpoint algorithm (Section 2).• We propose an approach to modularize the description of big-step fixpoint algorithms through sound and reusable fixpoint combinators (Section 3).• We present a library of reusable fixpoint combinators that serve as building blocks for developing fixpoint algorithms (Section 4).• We develop a formal theory for these combinators that allows us to prove their soundness separately and once and for all (Section 5).• We demonstrate that our approach is feasible and useful by implementing it as part of the Sturdy framework (Section 6).

DESIGNING BIG-STEP FIXPOINT ALGORITHMS
In this section, we first describe conditions that guarantee the termination of big-step fixpoint algorithms.In the second half, we develop a big-step fixpoint algorithm that satisfies these conditions.Schmidt [1995] introduced big-step abstract interpretation and showed how to compute its fixpoint.We reformulate his findings as three conditions that guarantee the termination of big-step fixpoint algorithms.Consider the analysis of the factorial function implemented in a language with firstorder functions.The following diagram shows a big-step reduction trace of an abstract interpreter with unbounded recursion, where ⊢ ⇓ evaluates an expression under environment to an abstract value .Such a trace looks similar to the trace of a concrete big-step interpreter, except that the values are intervals.

Enforcing Termination of Big-Step Fixpoint Algorithms
The analysis starts at the call fact(n), where n is bound to the interval [0,∞] in the environment.
Because the interval [0,∞] contains 0 and other numbers, the abstract interpreter has to evaluate both branches of the conditional if(n == 0) and join the results.Whereas the analysis of the first branch terminates after only one step, the second branch diverges while recurrently calling the factorial function with the same environment over and over again (see highlighted calls).We write the question mark symbol to represent that the abstract interpreter diverged and did not produce a result.This leads us to the first condition: Condition 1 A big-step fixpoint algorithm has to detect recurrent recursive calls and cut off recursion to avoid non-termination.
Detecting recurrent calls allows the fixpoint algorithm to iterate that part of the computation that spans the initial call and the recurrent call.One way of detecting recurrent recursive calls is to remember the calls of the abstract interpreter on each branch of the derivation tree.Each call consists of the inputs of the abstract interpreter, e.g., an expression and an environment.By remembering the calls, we can easily detect a diverging call, if the exact same call occured earlier, further down the derivation branch.However, this way of detecting recurrent recursive calls is insufficient.For example, consider the analysis of the factorial function for negative arguments.Clearly, the factorial function does not terminate for negative arguments, and we expect the abstract interpreter to return an analysis result that represents non-termination.Instead, the abstract interpreter itself diverges: The abstract interpreter analyzes the factorial function with smaller and smaller intervals, because factorial decrements its argument on every recursive call.Even though the intervals become smaller, the chain of recursive calls is still infinite.Therefore, the fixpoint algorithm never encounters a recurrent recursive call.This means that a fixpoint algorithm that satisfies the first condition still may not terminate.This leads us to the second condition: Condition 2 A big-step fixpoint algorithm has to ensure that all possibly infinite call chains have a recurrent call.
In other words, all call chains are either finite or repeat themselves after finitely many calls.This ensures that a fixpoint algorithm can find a recurrent call even in infinite call chains.
While the first and second condition concern the inputs of the abstract interpreter, the third condition concerns its outputs.To illustrate this condition, consider an interval analysis of the multiplication function on Peano numbers, where we initially bind m to [1,∞] and n to [1,1].
The right-hand side branch of the derivation tree contains a recurrent call of mult.In this example, we represent the result of the recurrent call with a symbolic variable .By tracing back the result to the initial call of mult, we obtain the recursive equation ) .An established technique for solving such an equation is to start with the empty interval ⊥ and then to proceed iteratively until reaching a fixpoint [Cousot and Cousot 1992].However, starting with ⊥, this technique does not reach a fixpoint in a finite number of steps for our example: . .This example shows that even if a big-step fixpoint algorithm ensures and detects recurrent calls, it still might iterate on the analysis result indefinitely.This leads us to the third condition: Condition 3 A big-step fixpoint algorithm may only iterate the results a finite number of times.
Our three conditions guarantee termination: Theorem 2.1 (Termination).If a big-step fixpoint algorithm satisfies the three termination conditions and all reduction rules have a finite branching factor, then the big-step fixpoint algorithm terminates.
Proof.Condition 1 and 2 ensure that each infinite call chain is eventually cut off at a recurrent call and hence is finite.Condition 3 ensures that the fixpoint algorithm iterates on the analysis result for each node of the tree finitely many times.Finite call chains, finite iteration, and the finite branching factor of the rules guarantee that the big-step derivation tree is finite.Therefore, the fixpoint algorithm terminates.□ To summarize, a big-step fixpoint algorithm terminates if it satisfies the termination conditions.In the following subsection, we describe a big-step fixpoint algorithm that satisfies the termination conditions.

A Big-Step Fixpoint Algorithm that iterates on Strongly-Connected Subgraphs
In this section, we describe a novel fixpoint algorithm for big-step abstract interpreters that iterates on the strongly-connected subgraph of the graph-shaped trace of the abstract interpreter.The fixpoint algorithm targets the simple functional language from Section 2.1, but we generalize it in Section 3 by making it language-independent and modular.
The fixpoint algorithm iterates on the strongly-connected subgraphs (SCGs) of the graph-shaped trace of the abstract interpreter [Bourdoncle 1993].An SCG is a set of calls from which it is possible to reach all other calls in the same set.SCGs in the abstract interpreter trace occur if the analyzed program has cyclic dependencies, such as loops or recursive functions.For example, consider the graph-shaped trace to the right for the analysis of the SCG and an inner SCG indicated by the differently shaded areas.The solid arrows indicate calls and returns in the order in which they are executed by the abstract interpreter.The dotted arrows indicate recurrent calls.A call is recurrent when that same call is already on the call stack.In our example, we have three recurrent calls and the dotted arrow indicates the outermost dominator.We explain later in Figure 2 how this trace is computed in more detail.
To compute a fixpoint, the algorithm has to iterate on all calls in the body of an SCG.The order in which the fixpoint algorithm iterates over the calls does not matter for soundness [Bourdoncle 1993], but affects performance and precision of the analysis.In this section we present an algorithm that prioritizes calls in the innermost SCGs, before iterating on the outer SCGs.The trace of the abstract interpreter only becomes known while the analysis is running.Hence, the fixpoint algorithm cannot compute SCGs a priori and instead it must discover SCGs on the fly while the analysis is running.To detect SCGs, our algorithm tracks recurrent calls, because some recurrent calls are the entry calls of SCGs.For example, in the trace of the Fibonacci function above the recurrent call fib[0,∞] points to the entry call of the inner SCG (rightmost dotted arrow), whereas the recurrent calls of fib[1,∞] point to the entry call of the outer SCG.To detect the innermost SCGs, the algorithm looks for the first recurrent call that it encounters upon returning.
Listing 2 shows the adapted abstract interpreter eval and the main fixpoint algorithm fix monolithic .Instead of calling itself recursively like in Listing 1, the abstract interpreter eval calls fix monolithic to evaluate subexpressions, and fix monolithic calls eval mutual recursively.This allows us to encapsulate the fixpoint logic in fix monolithic , whereas eval captures the rest of the abstract language semantics, which we do not show for brevity.Our fixpoint algorithm uses three data structures: A map Stack to detect recurrent calls, storing for each expression Expr the abstract environment Env under which the expression is evaluated.A map Cache to iterate on analysis results, storing for each abstract Call the abstract value Val to which the call evaluated.A set SCG to detect which calls need to be iterated, containing recurrent recursive calls.
The algorithm first checks in line 9, if the expression is a function body and hence a potentially diverging call.If the expression is not a function body (e.g., a numeric operator), no iteration is necessary to find a fixpoint and we can simply call eval.This not only saves analysis time, but also reduces the size of the stack and cache tremendously.If the expression is a function body, the algorithm then checks if the cache contains a stable analysis result for the call and returns this result to avoid redundant reanalysis (line 10).Analysis results are stable if they do not grow anymore when reevaluated and if they solely depend on other stable analysis results.If the cache only contains an unstable or no analysis result, the algorithm checks if the call (env,expr) is a recurrent call by searching for it on the stack.In case of a recurrent call, the algorithm satisfies Condition 1 by either returning the unstable analysis result (line 11) or returning ⊥ (line 12).Furthermore, since the analysis result needs to be iterated on, the algorithm adds its call to the SCG set.If the call does not appear on the stack, the algorithm calls a recursive helper function iterate (line 13) that iterates the analysis result until it stabilizes.
Function iterate is responsible for iterating on calls in SCGs.The first line of iterate applies a widening operator [Cousot and Cousot 1992] ∇ Stack to the stack and the call.This widening operator ensures that all infinite non-repeating stacks eventually have a recurrent call (Condition 2).We explain this operator in more detail below.In line 19, the algorithm calls the abstract interpreter eval with the widened inputs.The algorithm then iterates on the call, in case the call is a head of an SCG (line 20), or otherwise simply returns the result of eval (line 28).In line 22, the algorithm uses a widening operator for values ∇ Val to ensure that the analysis result does not grow indefinitely (Condition 3).If the widened value is strictly greater than the cached value, the algorithm keeps iterating (line 26).Otherwise, if the widened value did not grow anymore, the algorithm terminates Listing 2. Big-step fixpoint algorithm iterating on the innermost strongly-connected subgraph.The code uses common mathematical notation for operations on maps and sets for readability.In particular, the notation cache(call) looks up the key call in the map cache and the notation cache[call ↦ → res] updates the map entry call to res.Furthermore, {call} refers to the singleton set with the element call.
the iteration, returns the widened value, and removes the call from the SCG since it does not require iteration anymore (line 27).
The widening operator ∇ Stack ensures that all infinite non-repeating stacks eventually have a recurrent call (Condition 2).If the expression appeared on the stack and the environment of the call is smaller than the environment on the stack (line 32), the stack widening operator introduces a recurrent call by reusing the environment on the stack.If the environment on the stack is not an upper bound of the environment in the call (line 33), the stack widening operator applies a widening operator ∇ Env to both environments.Operator ∇ Env computes an upper bound of both environments and ensures that the environment under which an expression is evaluated cannot grow infinitely.Lastly, in case the expression did not occur on the stack (line 34), the operator adds the call to the stack without changing it.
We illustrate how this algorithm works at an example of the analysis of the Fibonacci function.Figure 2 shows a trace of the abstract interpreter starting at fib[1,∞].To make the internals of the fixpoint algorithm visible, we write ↰ ⟨ , , ⟩ for a call with stack and cache .Furthermore, we write ↱ ⟨ , , ⟩ for a return from a call with result value , output cache , and SCG .Sometimes we show intermediate steps in the abstract interpretation, where we allow to be an expression that evaluates to an interval.The highlighting indicates which analysis results changed between consecutive iterations.
The algorithm alternatingly iterates the innermost and the outermost SCG shown in the bottom right of Figure 2. In the beginning the algorithm explores the recursive calls of the fibonacci function until it hits the recurrent calls (lines 2, 4, and 5 in Figure 2).In these cases the algorithm returns ⊥ to avoid non-termination and adds the call to the SCG set (line 12 in Listing 2).Later the algorithm returns to the call of fib[0,∞] (line 6 in Figure 2) and iterates because the call is in SCG set 2 and the result interval has grown from ⊥ to [0,1] (lines 20 and 25 in Listing 2).The following iteration propagates the new analysis result fib[0,∞] ↦ → [0,1] throughout the inner SCG.After returning to call fib[0,∞] (line 10 in Figure 2), the result did not grow and the algorithm returns to the surrounding call fib[1,∞], removing call fib[0,∞] from the SCG set (line 27 in Listing 2).Since the result for fib[1,∞] has grown from ⊥ to [1,1], the algorithm iterates again and propagates the new analysis result fib[1,∞] ↦ → [1,1] throughout the outer SCG.After returning to call fib[0,∞] (line 17 in Figure 2), the result has grown from [0,1] to [0,2] and hence the algorithm widens the result to [0,1]∇ Val [0,2] = [0,∞] (line 22 in Listing 2).Since the result is greater after widening, the algorithm iterates the call fib[0,∞] again until it does not grow anymore.Finally, after one more iteration of call fib[1,∞], the result [1,∞] does not grow anymore (line 25 in Figure 2) and the algorithm sets the cache entry to stable (line 23 in Listing 2).
In summary, we developed a big-step fixpoint algorithm that iterates on the strongly-connected subgraphs of the graph-shaped trace and satisfies the termination conditions.We prove soundness of this algorithm in Section 5.

MODULAR DESCRIPTION OF BIG-STEP FIXPOINT ALGORITHMS
In the previous section, we discussed a big-step fixpoint algorithm that iterates on the stronglyconnected subgraphs of the graph-shaped trace.Even though the initial fixpoint algorithm works and is sound, it is hard to specialize and fine-tune it.We can solve these problems by modularizing the description of big-step fixpoint algorithms, which we discuss in this section.We illustrate the modular description by refactoring the function fix monolithic into smaller reusable fixpoint combinators.Additionally, this modularization will enable us to prove soundness of the fixpoint algorithm modularly, which we discuss in Section 5.
Language-Independence.The problem that makes function fix monolithic language-dependent is that it refers to the abstract interpreter eval, environments, expressions, and values from the analyzed language directly.To make the algorithm language-independent, we first remove references to language-specific types.As first step, we replace the inputs ( Env,Expr) and outputs Val of the abstract interpreter with the type variables a and b.As second step, we remove the reference to eval by turning it into an open-recursive style and passing its body as an argument to fix monolithic .This allows us to implement fix monolithic independently of the analyzed language.
Reusable Fixpoint Combinators.To make the fixpoint algorithm easier to specialize to a new analysis, we make two more changes.First, instead of implementing one single monolithic fixpoint algorithm, we split its functionality across multiple smaller fixpoint combinators 1 , . . . .These combinators are then called by a function fix in a round-robin fashion, such that each combinator has the chance to affect the fixpoint computation: In particular, fix ( eval rec . ..) invokes combinator .Combinator first invokes combinator 1 , which then may invoke 2 , and so on, until eventually calls ( eval rec . ..) (fix ( eval rec . ..)) and the cycle repeats.
Even though this design of fixpoint combinators allows us to separate concerns, their type a ⇓ b is not fully extensible, as some combinators may need some extra data not present in the stack or the cache.Therefore, as second change, we generalize the type a ⇓ b to an arrow type c a b [Hughes 2000].The arrow type reads as "some effectful computation c that takes values of type a as input and produces values of type b as output."Arrows allow us to implement fixpoint combinators without having to refer to a specific type of fixpoint computation.They are particularly useful for implementing big-step fixpoint algorithms, because they cleanly separate the inputs of an effectful computation from the outputs.Moreover, they have proven useful for modularizing other parts of the abstract interpreter [Keidel andErdweg 2019, 2020;Keidel et al. 2018].
Refactoring the fixpoint algorithm.Based on these principles, we now refactor the fixpoint algorithm fix monolithic into three reusable combinators innermost , filter , and stackWiden .
The combinator innermost is a stripped down version of the fix monolithic algorithm and only satisfies Condition 1 and 3.The combinator is parameterized by operations to access and modify the stack, cache and SCG contained in the effectful arrow computation.Furthermore, the code uses the following arrow notation: The keyword proc x introduces a new arrow computation that binds its argument to the variable x.The syntax y ← f x calls an arrow computation f with the argument x and binds the result to the variable y.Lastly, the keyword return x returns x as result of the arrow computation, but does not exit the surrounding proc like regular returns.
The combinator innermost first looks up the call in the cache.If the cached result is stable, the combinator simply returns the cached entry (line 5).Otherwise, the combinator looks up the call on the stack (line 7).In case of a recurrent call (line 8), the algorithm adds the call to the SCG and returns the cached entry.Otherwise, if the call did not appear on the stack (line 11), the algorithm calls the recursive helper function iterate that updates the analysis result until it does not grow anymore.The function iterate first calls the computation f while adding the current call to the stack (line 19).Afterwards, it checks if the call occurred in the SCG (line 20) and hence needs to be iterated on.If the call occurred in the SCG, function iterate updates the cache with the new result (line 23).The operation Cache.updatesimultaneously updates the cache, widens the new result against an existing entry and checks if the result is stable or has grown.If the analysis result has grown the function iterates again (line 24).Otherwise, it removes the call from the SCG and returns the widened result (line 31).If the SCG consist of only a single element, i.e. the current call, then the current result only depends on other stable analysis results.
To address Condition 2, we implement a fixpoint combinator that applies a widening operator to the current stack and call: Combinator stackWiden first accesses the stack contained in the arrow computation.It then applies the stack widening operator (∇ Stack ) to this stack and current call.Afterwards, it passes the widened call to the computation f and sets the new stack.Lastly, the higher-order fixpoint combinator filter , inspired by Wei et al. [2019] fix_select, filters out calls not relevant to the rest of the fixpoint algorithm: The combinator filter either calls the combinator whenever the predicate holds, or skips the combinator when the predicate does not hold.With these three fixpoint combinators, we can recreate the fixpoint algorithm fix monolithic from the previous section: The execution order of the combinators is from outside inwards: First filter gets control, then stackWiden , then innermost , and finally ( eval rec . ..) before the cycle repeats.
To summarize, in this section we proposed a modular description of fixpoint algorithms.In particular, we describe fixpoint algorithms with reusable fixpoint combinators, where each combinator captures a certain aspect of the fixpoint algorithm.

A LIBRARY OF REUSABLE FIXPOINT COMBINATORS
In the previous section, we described a framework for developing modular fixpoint algorithms by the means of fixpoint combinators.Based on this framework, we develop a library of reusable fixpoint combinators in this section, which serve as building blocks for fixpoint algorithms.

Iteration Strategy Combinators
An iteration strategy determines the order in which statements are analyzed.For example, the combinator innermost of Section 3 iterates on the innermost SCGs of the graph-shaped trace.Furthermore, an iteration strategy cuts off recurrent recursive calls to enforce termination, at the cost of precision.
Iterating on the outermost SCGs.For some programs, it can be faster to iterate on the outer SCGs first.For example, consider the analysis of the follow program: for(j = 0; j < n; j++) { g(x + j); } } for(i = 0 . ..) . . .
We implement this iteration strategy with the fixpoint combinator outermost : The combinator outermost is similar to innermost , except that it returns to the head of the outermost subgraph before iterating.The combinator identifies heads of the outermost subgraph by checking that the size of the SCG set is 1 (line 9).This check works because the combinator removes each call from the SCG set when returning (line 14) and if this set has only a single call left, this call must be the head of an outermost SCG.
Iterating on the topmost call.The combinators innermost and outermost iterate on the SCGs of the graph-shaped trace of the abstract interpreter.However, calculating the SCGs induces an overhead, which slows down the analysis especially for programs, that only consists of a single large SCG.In these cases, it can be faster to only iterate on the topmost call of the abstract interpreter.We implement this iteration strategy with the following fixpoint combinator, which is inspired by an existing big-step fixpoint algorithm [Darais et al. 2017]: The combinator topmost neither requires a stack nor SCGs.Instead, it uses the cache to detect recurrent calls (line 2).On the topmost call of the abstract interpreter, the combinator compares the cache of the current iteration to the cache of the previous iteration (line 18).If the cache of the current iteration has grown, the combinator keeps iterating with a new empty cache (line 22).The nextIteration operation additionally widens the call, which allows data like a monotone store to be passed along between iterations.
Mixing iteration strategies.Our case studies show that it is difficult to find a single iteration strategy that works best for all programs (Section 6.3).To this end, the following combinator mixes two iteration strategies 1 and 2 on a case-by-case basis: The predicate is an effectful computation, which allows it to dynamically adapt the iteration strategy.
For example, the predicate may first try both strategies 1 and 2 once to decide afterwards based on collected performance metrics.

Recursion Depth Combinators
Recursion depth operators control how deep an abstract interpreter recurses on a program.For example, the combinator stackWiden of Section 3 limits the recursion depth to be finite by widening calls on the stack.
Call-site context sensitivity.A popular technique to limit the recursion depth of the abstract interpreter is to join all calls with the same -truncated call string at the cost of precision [Shivers 1991].We implement this technique with the following combinator: The combinator callsiteSensitive uses a context cache that remembers the call for the current context (line 5).If the context cache contains a larger call than the current call, the combinator calls computation f with the cached call (line 7).Otherwise, the operation Cache.updatewidens the current and cached call and calls computation f with the widened call (line 10).Note that we do not need to change the analysis itself to integrate call-site sensitivity, unlike Shivers [1991].Instead, we simply add this combinator to the fixpoint algorithm.
Stack unfolding.The following combinator improves precision by unfolding the first few calls on the stack to prevent joining: The combinator unfold recursively calls computation f as long as the stack size is below a certain limit and falls back to the combinator if the stack size exceeds the limit.We can integrate this combinator into a fixpoint algorithm by applying it to a stack widening combinator ( unfold 10 ( stackWiden ∇ Stack )).This prevents joining of the stack for the first 10 recursive calls.
Loop unrolling.Another common technique to improve precision is to unroll the first few iterations of a loop to prevent joining [Mauborgne and Rival 2005] With this observation, we implement a combinator that only joins after the same call appeared a certain number of times on the stack: Similar to the previous combinator, we can integrate this combinator by applying it to a stack widening combinator: unroll 10 ( stackWiden ∇ Stack )

Tracing Combinators
Tracing combinators run alongside the main fixpoint algorithm to record auxiliary data like a trace, without affecting precision.
Recording a control-flow graph.The following tracing combinator records a control-flow graph (CFG) of the program, which describes the order in which statements are evaluated:

addEdge (predecessor, call) withNewPredecessor f call
Since the control-flow of a program is encoded implicitly in the big-step abstract interpreter, all the combinator needs to do is to add an edge to the CFG between the most recently evaluated call predecessor and the active call.Afterwards, the combinator passes control to computation f, remembering the active call as new predecessor.We can integrate this combinator into an existing fixpoint algorithm by adding it to the front ( CFG • filter isFunctionBody (. . .innermost . ..)).In this case, the CFG contains all statements as nodes.We can control the granularity of the CFG by changing the position of the CFG combinator ( filter isFunctionBody ( CFG • . . .innermost . ..)).In this case, the CFG only contains function calls, in other words, the CFG is an interprodcedural call graph.
Debugging static analyses.The following tracing combinator allows debugging of analyses with a graphical debugger [Pree 2020]: The fixpoint combinator runs as a server within the fixpoint algorithm and sends information to a graphical debugging client in a browser.On a break point the combinator sends the current stack and the CFG to the debugging client (line 5).After the client returned a debugging command, the combinator executes the command and calls computation f.This resumes the analysis until the combinator hits the next break point.

MODULAR SOUNDNESS PROOFS OF BIG-STEP FIXPOINT ALGORITHMS
In this section, we develop a formal theory to prove soundness of big-step fixpoint algorithms that consist of fixpoint combinators.In particular, we prove that a modular fixpoint algorithm is sound if all of its combinators are sound.
To this end, we first review a soundness proof of monolithic fixpoint algorithms, which we later extend for algorithms: Proposition 5.1 (Soundness of Monolithic Fixpoint Algorithms [Cousot and Cousot 1992]).Let eval : → be an abstract interpreter and eval : → be the monotone collecting semantics of the concrete interpreter over two complete lattices ( , ⊑, ⊔) and ( , ⊑, ⊔).Furthermore, let : → be a monotone concretization function such that ∀ ∈ .∈ .⊑ ( ) =⇒ eval( ) ⊑ ( eval( )).A fixpoint algorithm for the abstract interpreter is sound if it yields a post-fixpoint of eval, i.e. an element ∈ with eval( ) ⊑ . Proof.

eval( ) ⊑
( is a post-fixpoint of eval) =⇒ lfp(eval) ⊑ ( eval( )) (Tarski's fixpoint theorem [Tarski 1955]) □ This proposition requires that the collecting semantics of the concrete interpreter is monotone, such that the least fixpoint lfp(eval) exists.Monotonicity of the collecting semantics follows directly by Scott-continuity of the denotational semantics of the concrete interpreter [Streicher 2006].
We build on this idea to develop a soundness composition theorem for modular big-step fixpoint algorithms.Let us first assume that the modular fixpoint algorithm is built from fixpoint combinators over the following grammar: This grammar allows us to formulate the following soundness lemmas for each type of combinator: That is, an atomic fixpoint combinator is sound if all post-fixpoints of • are also post-fixpoints of , for any .All other types of combinator preserve this post-fixpoint property.
These soundness lemmas allow us to prove soundness of modular fixpoint algorithms once and for all with the following theorem: Theorem 5.2 (Soundness of Modular Fixpoint Algorithms).A modular fixpoint algorithm fix ( eval) is sound, if all of its combinators are sound.
Proof.We prove by structural induction over that ∀ , .( ( )) ⊑ =⇒ ( ) ⊑ .By definition of fix, we get ( eval(fix ( eval))) = fix ( eval), which satisfies the precondition above.We conclude eval(fix ( eval)) ⊑ fix ( eval), which shows that fix ( eval) is a postfixpoint of eval.Thus the fixpoint algorithm fix ( eval) is sound by Proposition 5.1.□ This way of proving soundness of modular fixpoint algorithms is more flexible than a monolithic proof because it allows us to reorder and add new combinators without invalidating the soundness proof.

Soundness Proof Strategies for Fixpoint Combinators
In this subsection, we prove three theorems that guarantee soundness of three classes of combinators.The soundness proofs are split into two parts: One part proves soundness of classes of combinators that satisfy certain properties (Theorem 5.3 and 5.4) and one part shows combinator implementations actually satisfy these properties (Corollary 5.4 and 5.6).
Proof.We assume innermost ( ( )) ⊑ for all monotone and for all and call.The key insight is that unstable intermediate results only occur within SCGs and innermost does not return until all calls within the SCG have reached a post-fixpoint.To this end, we first prove that either ( ) call ⊑ call or that the call appears on the stack and in the resulting SCG set.
• If result cached is unstable and the call does not occur on the stack, combinator innermost iterates until result new does not grow anymore (line 24 in innermost ) and result new ⊑ result cached .
In this case, combinator innermost removes the call from the SCG set and returns result widened (line 31).By the assumption, it follows that return result widened ⊑ call.By transitivity we conclude ( ) call ⊑ return result new ⊑ return result widened ⊑ call.• If result cached is unstable and the call occurs on the stack, combinator innermost adds the call to the SCG (line 9).• If result cached is stable, combinator innermost simply returns the cached result (line 5).By the assumption, it follows that return result cached ⊑ call.Furthermore, the Cache.updateoperation only marks a result as stable if ( ) call ⊑ return result cached , as explained in the previous case.By transitivity we conclude ( ) call ⊑ return result cached ⊑ call.When we run a fixpoint algorithm including innermost , we initialize the stack to be empty.This means that call cannot appear on the stack and hence ( ) call ⊑ call has to be true.□ Theorem 5.8.The fixpoint combinator outermost is sound.
Proof.The argument for outermost is similar to innermost .The only difference is that outermost waits until the SCG contains only a single element.But this does not change that it only returns from an SCG until all calls within reached a post-fixpoint.□ To summarize, in this section we presented a way to prove soundness of fixpoint algorithms that consist of fixpoint combinators.In particular, a fixpoint algorithm is sound, if all of its combinators are sound.This simplifies the soundness proof, as it suffices to prove each combinator sound individually.Furthermore, we proved soundness of three classes of fixpoint combinators.

CASE STUDIES
In this section, we evaluate the feasibility of our framework for modular fixpoint algorithms.In particular, we integrated the combinators of Section 4 into the Sturdy library [Keidel and Erdweg 2019;Keidel et al. 2018] and used them to develop fixpoint algorithms for the following analyses: • A static type analysis [Keidel and Erdweg 2020] for Stratego [Visser et al. 1998], a domain-specific dynamically-typed language for program transformations, • a dead-code constant-propagation analysis for WebAssembly, a low-level bytecode that runs in the browser [Haas et al. 2017], • a -CFA [Shivers 1991] for Scheme [Abelson et al. 1998], a dynamically-typed programming language with first-class functions and mutable state.The goal of these case studies is to asses the language and analysis-independence, the precision, and the performance of the fixpoint combinators.

Static Type Analysis for Stratego
In our first case study, we implemented a fixpoint algorithm for a static type analysis [Keidel and Erdweg 2020] for Stratego [Visser et al. 1998], a domain-specific dynamically-typed language for developing program transformations.Stratego is difficult to type statically because of features like generic program traversals that temporarily produce ill-typed programs.
The abstract interpreter takes a Stratego program called a strategy, a strategy environment, a term environment, and a program term that is transformed.Furthermore, the abstract interpreter returns as output a list of errors, an updated term environment and resulting term.If the strategy fails to match a term pattern, the abstract interpreter returns the empty tuple instead.Type-checking Stragego program transformations is not trivial since generic program traversals may produce intermediate terms that are ill-sorted.To solve this, the analysis uses an abstract domain that is able to represent ill-sorted terms: This abstract domain is infinite since abstract terms can grow infinitely deep.To this end, a widening operator cuts-off terms at a specified depth, trying to type check deeper terms to determine their sort.
The fixpoint algorithm uses the outermost iteration strategy and applies a stack widening operator because the abstract term domain and term environment are infinite.The stack widening operator replaces the current call with the topmost call on the stack that is greater.To debug the analysis during development we added a tracing combinator trace within the filter expression to print a trace of analyzed strategies and their abstract term arguments.Furthermore, we occasionally moved the tracing combinator to the outside of the filter expression for a more fine grained trace that contains all substrategies as well.
We tested the abstract interpreter and its fixpoint algorithm on a test suite with 61 test cases including 3 existing program transformations: an desugaring of arrows (665 lines of code)6 [Paterson 2001], a normalization of arrows to causal commutative normal form (490 loc) [Liu et al. 2009], and an interpreter for PCF (61 loc) [Plotkin 1977].We found that in all of these test cases the abstract interpreter terminates with a sound analysis result.Furthermore, the results where precise enough to validate the well-typedness of the 3 program transformations.
This case study shows that the modular fixpoint algorithm is precise enough to yield usable analysis results.

Dead-Code Constant-Propagation Analysis for WebAssembly
In our second case study, we implemented a fixpoint algorithm for a dead-code constant-propagation analysis for WebAssembly (Wasm), a low-level bytecode that runs in the browser [Haas et al. 2017].This analysis can be used to reduce the size of the executables that are sent to the browser.
The abstract interpreter takes as input a list of instructions, a list of return types, a module instance, an operand stack, a frame of local variables, function tables, module memories, a global state and a set of errors: eval :: ... ⇒ c ([Instr],[Type],ModuleInst, OperandStack, Frame, Tables, Memories, GlobalState, Errors) ( OperandStack, Frame, Memories, GlobalState, Errors) eval = fix ( CFG • filter isLoopOrCall innermost ) ... The abstract interpreter abstracts values with a finite constant abstract domain, i.e., abstract values are either 32 or 64-bit integers, 32 or 64-bit floating point numbers, or ⊤.Furthermore, operand stack and call frame are abstracted with lists of abstract values, because their shape is statically known [Haas et al. 2017].Moreover, the linear memory is abstracted with a byte-indexed vector of abstract values.
The fixpoint algorithm uses the innermost iteration strategy and applies it to loops and function calls.The combinator CFG records an inter-procedural control-flow graph (CFG) which allows us to find dead-code, i.e., code that will never be executed.In particular, we remove all instructions that do not appear in the CFG because they cannot be executed.This works because the control-flow of the abstract interpreter overapproximates the control-flow of the concrete interpreter and an instruction not analyzed by the abstract interpreter cannot be executed by the concrete interpreter.
Our Wasm analysis and the fixpoint combinators have been reimplemented in Scala [Brandl et al. 2023].The analysis has been evaluated on 1458 binaries of the WasmBench benchmark suite [Hilbig et al. 2021].With a timeout of 60 seconds, the dead-code constant-propagation analysis terminates on average in 5s and eliminates 20% of the program code.In contrast, the industry-standard Binaryen terminates on average in 0.1s, but only eliminates 9% of the program code.
This case study demonstrates two points: • Our fixpoint combinators are meta-language, object-language, and analysis independent.
• Our fixpoint combinators scale to develop fixpoint algorithms for real-world languages.

6.3
-CFA for Scheme For our third case study, we implemented an inter-procedural control-flow analysis ( -CFA) [Shivers 1991] and static type analysis for Scheme [Abelson et al. 1998], a dynamically-typed real-world programming language with first-class functions and mutable state.The abstract interpreter takes as input a list of expression, an environment, store and errors and returns as output an abstract value, store and errors: Even though Scheme is a dynamically-typed language, the abstract domain above typically used for statically-typed languages.Specifically, two abstract values of different types join to ⊤.This choice of abstract domain is precise enough to soundly compute the control-flow of all but one benchmark programs we discuss below, but performs better than a set-based abstraction.
As fixpoint algorithm, we first developed an initial algorithm that we later specialize.The initial fixpoint algorithm uses topmost as a baseline iteration strategy because it does not compute SCGs similar to Shivers's -CFA.Furthermore, the algorithm uses the combinator recordCallSite to record the most recent call sites, which we use as abstract addresses.
While the initial fixpoint algorithm terminates and is sound, it converges to a fixpoint very slowly.The reason is that the algorithm "forgets" about recent store and error updates when it returns a cached result.To address this problem, we specialize the fixpoint algorithm to use a different cache that respects the part of the input and output that only ever grows.The following code shows the lookup operation of the cache: The lookup operation of MonotoneCache always returns the new and greater element monotone new .
The cached element monotone old is only kept to determine if the result is stable.Returning the old cached element monotone old would forget about the store and error updates.
We integrate this improvement into the initial fixpoint algorithm by replacing the cache that is contained in the arrow computation.Furthermore, we use the combinator transform to group the parts of the input and output that grow monotonically: The combinator transform applies two isomorphisms to the inputs and outputs of the abstract interpreter such that variable call has type ([Expr], Env) and variable monotone new has type ( Store, Error) within the Cache.lookupoperation above.
While the monotone cache improves the performance of the fixpoint algorithm, there is still room for fine-tuning the iteration strategy.In particular, we evaluate 3 different iteration strategies by analyzing Scheme programs of the Gabriel [Gabriel 1985] and Scala-AM benchmark suite [Es et al. 2019].The Gabriel benchmark suite contains 9 Scheme files from 17 up to 562 lines of code (loc) with an average of 137 loc. 7The Scala-AM benchmark suite contains 5 Scheme files from 10 up to 40 loc with an average of 26 loc.The analysis is precise enough to soundly analyze the control-flow of all benchmark programs, except for dderiv where the analysis tries to call a closure which is ⊤.
Figure 3 shows the speedups over the baseline iteration strategy topmost of our initial fixpoint algorithm.Benchmarks like cpstak and diviter have an SCG at the very top of the program.This means there is no significant difference between the iteration orders and the overhead of computing the SCGs slows down the iteration strategies innermost and outermost .On other benchmarks like destruc, takl, and rsa the SCGs are smaller and do not span the entire program.In these cases the iteration strategies innermost and outermost get an considerable speedup.The results show that no single iteration strategy performs best for all analyzed programs and further fine-tuning is needed.
Lastly, we assess the potential performance overhead caused by the modularization of the fixpoint algorithm.In particular, we inspected the low-level code of the fixpoint algorithm generated by the Haskell compiler GHC.The GHC compiler first inlines the definitions of the fixpoint combinators and arrow computation and then optimizes the residual code.The result is a pure function close to a hand-written monolithic fixpoint algorithm, meaning that modularization has no performance penalty.Fig. 3. Normalized running times of different iteration strategies for a 0CFA analysis for Scheme.The plot shows the speedup of each iteration strategy over the baseline topmost (higher is be er).The error bars show the standard deviation of the ratio distribution for the normalized running time.
To summarize, the -CFA case study demonstrates three points: • We were able to specialize the initial fixpoint algorithm without changing code of existing fixpoint combinators.• We compared the performance of 3 different iteration strategies and concluded that no iteration strategy performs best for all programs.This shows that further fine-tuning of the iteration strategy is needed.• The Haskell compiler optimizes the modularized fixpoint algorithm by inlining and produces code close to hand-written monolithic fixpoint algorithm.Thus, modularization does not inflict a performance penalty.

RELATED WORK
The focus of this work is the modular description of fixpoint algorithms for big-step abstract interpreters.In this section, we discuss work related to our approach presented in this paper.
Modularizing the Definition and Soundness Proofs of Big-Step Abstract Interpreters.There have been several works that modularized different parts of the definition and soundness proofs of big-step abstract interpreters.Keidel et al. [2018] describe an approach that modularizes the concrete and abstract language semantics and its soundness proof with arrows [Hughes 2000].In particular, the concrete and abstract semantics is derived from the same generic interpreter that is composed of a number of primitive operations over values, stores, exceptions, etc.The benefit of this approach is that it guarantees that an entire analysis is sound, as long as each operation is sound.However, Keidel et al. [2018] do not show a fixpoint algorithm nor do they describe how a fixpoint algorithm should be implemented.Bodin et al. [2019] describe a similar approach that derives both the concrete and abstract semantics from the same skeletal semantics.However, compared to arrows used by Keidel et al. [2018], they use a more liberal algebra called skeletons, which consists of hooks, filters, and branching operations.Yet, they provide similar soundness guarantees: an entire analysis derived from a skeletal semantics is sound, as long as all of its operations are sound.Bodin et al. [2019, Section 5.4] define the abstract semantics as the greatest fixpoint of the abstract collecting semantics.However, they do not show an algorithm that computes this fixpoint, nor do they explain how such an algorithm can be described modularly.Keidel and Erdweg [2019] describe an approach that modularizes the effects of the analyzed language, such as exceptions and store mutations.More specifically, the approach captures the analysis of each effect with an analysis component which consists of a concrete and abstract arrow transformer.This approach simplifies the analysis of languages with multiple effects that interact with each other.Keidel and Erdweg define a single analysis for the fixpoint algorithm.However, they do not describe the fixpoint algorithm itself, nor do they describe how it can be decomposed further.In the present work, we make use of arrows and arrow transformers to modularize the description of fixpoint algorithms by the means of sound and reusable fixpoint combinators.We use arrows to describe fixpoint combinators that are independent of the type of the fixpoint computation.This allows us to change the type of the fixpoint computation, without needing to change the definition of the fixpoint combinators.Darais et al. [2017] describe an approach that derives several collecting semantics from the same generic semantics with different combinators.These combinators, for example, collect a trace of the abstract interpreter, they collect expressions that are dead code, or they compute a fixpoint.These combinators inspired the style of fixpoint combinators we present in this paper, in that our fixpoint combinators have the same type as Darais et al. combinators. However, Darais et al. do not describe a formal theory for these combinators which makes it hard to reason about their soundness.In this work, we developed a framework for modular fixpoint algorithms that is based on fixpoint combinators.This framework allows us to describe a family of fixpoint algorithms that can be configured and fine-tuned more easily, as we show in our evaluation.Furthermore, we developed a formal theory about these algorithms which allows us to prove their soundness compositionally.
Fixpoint Algorithms for Big-Step Abstract Interpreters.The space of fixpoint algorithms for big-step abstract interpreters has not been extensively studied yet.Schmidt [1995Schmidt [ , 1998] ] describes one of the first fixpoint algorithms for big-step abstract interpreters that operates on the derivation tree.The fixpoint algorithm unfolds the abstract derivation tree until each branch either terminates or repeats itself.The algorithm detects recurrent calls of the abstract interpreter by memoizing parts of the abstract derivation tree.If the algorithm finds a recurrent node in a branch, it cuts off recursion to avoid non-termination which satisfies Condition 1. Furthermore, the fixpoint algorithm satisfies Condition 2 by joining the environments of repeating expressions with a widening operator that ensures that infinite recursive call chains have a recurrent call.However, many details about how this algorithm actual could be implemented are missing.Specifically, Schmidt does not explain how SCGs are calculated and on which calls the algorithm iterates.Instead, the algorithm generates a number of recursive equations, which then can be solved with an arbitrary iteration order to calculate the fixpoint.We combine Schmidt's solutions to the termination conditions to implement our initial fixpoint algorithm fix monolithic in Section 2, which we later modularize.However, instead of generating recursive equations, our algorithm fix monolithic specifies an iteration order, i.e., the algorithm iterates on the innermost SCGs of the trace of the abstract interpreter [Bourdoncle 1993].Darais et al. [2017] present another fixpoint algorithm for big-step abstract interpreters, similar to parallel fixpoint iteration.We implemented this algorithm in Section 4 with the combinator topmost .The algorithm uses two caches to remember the analysis result of two consecutive fixpoint iterations.The algorithm then iterates over the entire program, updating the cache of the most recent iteration.If none of the caches change anymore, the algorithm has reached a fixpoint and terminates.The algorithm satisfies Condition 1 by detecting recurrent calls if they have an existing cache entry.However, the algorithm does not satisfy the other two conditions which means that it does not terminate for infinite abstract domains.
Chaotic Fixpoint Iteration.We focus on chaotic fixpoint iteration because it is the most popular algorithm to solve a set of recursive equations in abstract interpretation [Amato et al. 2016;Bourdoncle 1993;Geser et al. 1994;Kim et al. 2020].Chaotic iteration strategies iterate on small parts of the analyzed program and hence are typically more efficient than parallel iteration strategies [Darais et al. 2017], which iterate on the entire analyzed program.On the downside, chaotic iteration strategies are more complicated to implement, because they need to keep track of SCGs.However, we were able to encapsulate this complexity within reusable combinators, which are easier to use.Bourdoncle [1993] presents a chaotic iteration order that is based on a weak topological ordering of the control-flow graph.The iteration order is computed before running the analysis, which requires knowing the control-flow graph ahead of time.The iteration order improves the precision as it reduces the number of widening points to the heads of SCGs.Bourdoncle [1993]'s work inspired the design of the fixpoint combinators innermost and outermost that we developed in this paper, as they use the same widening points.However, in contrast, our fixpoint combinators compute the iteration order dynamically while the analysis is running.This means our fixpoint algorithms do not need to know the control-flow graph ahead of time and can dynamically fine-tune and adapt the iteration order if needed.
Fixpoint Algorithms for Small-Step Abstract Interpreters.In contrast to big-step abstract interpreters, static analyses in small-step style have a longer history of research [Horn and Might 2010;Might and Shivers 2006a;Sergey et al. 2013;Shivers 1991].Similar to big-step abstract interpreters, small-step abstract interpreters also seamlessly combine data-flow and control-flow information.However, they describe the abstract semantics as a small-step relation.A fixpoint algorithm for such interpreters explores the finite state space of the small-step relation.Unfortunately, it is unclear how small-step fixpoint algorithms apply to big-step abstract interpreters, because of differences in the style of semantics: While small-step abstract interpreters use continuations to explicitly model control of the interpreter as part of the state space, big-step abstract interpreters leverage the control of the meta-language (e.g., Haskell).This means that big-step abstract interpreters cannot ensure termination simply by making the state space finite, because their interpreter function may diverge nonetheless.To this end, big-step fixpoint algorithms must detect recurrent recursive calls and iterate on them which is not necessary for small-step algorithms.

CONCLUSION
In this paper, we studied the modular description of fixpoint algorithms for big-step abstract interpreters.We identified three conditions that guarantee the termination of big-step fixpoint algorithms.Based on these conditions, we developed a fixpoint algorithm for big-step abstract interpreters that iterates on the strongly-connected subgraphs of the graph-shaped trace.However, since the algorithm consists of a single monolithic function, it is hard to extend, configure and adapt the fixpoint algorithm.To this end, we refactored the algorithm into small reusable fixpoint combinators which allow us to change the algorithm by rearranging and adding new combinators.Furthermore, the combinators simplify the soundness proof, as each combinator can be proved sound individually once and for all.Moreover, our evaluation demonstrates that our approach describes an entire family of fixpoint algorithms for different languages and analyses that can be easily extended, adapted and configured.Lastly, the fixpoint combinators have been reimplemented in Scala and used to develop fixpoint algorithms that scale to analyze real-world WebAssembly programs.

Fig. 1 .
Fig. 1.Monthly changes to fixpoint algorithms in the OPAL and TAJS analysis frameworks.

Fig. 2 .
Fig. 2. Example trace of the abstract interpreter analyzing the Fibonacci function.Arrow ↰ represents a call, arrow ↱ a return, and arrow ↫ an iteration.The highlighting indicates which results changed between consecutive iteration.The indentation level indicates the depth of the stack.