[Compiler Construction] Control Flow Graph, Liveness Analysis, Dataflow Analysis and Interference Graph, Implemented in OCaml
I am super busy, but I want to spend some time talking about how to write a compiler in ocaml that has liveness analysis, and can generate interference graph from CFG and liveness analysis, which is fundamental to doing correct, if not efficient, register allocation.
PeterYaoNYU/CompilerInOCaml
The Code can be found under the folder ps7, if not restructured after the writing of this post.
This is part of the NYU graduate level capstone project, with Prof. Joseph Tassarotti.
An interference graph is a prerequisite for building correct register allocation in a compiler. An interference graph relies on correct liveness analysis, and liveness is dependent on control flow graph, or cfg. CFG is a directed graph between blocks of code. The granularity of the block is adjustable, but to build efficient compiler that converges fast enough, one common optimization is to collapse codes together, with the last line of code in the cfg block being jump, return or if.
CFG AST
We have an ast very similar to assembly, but has an infinite number of registers to store temporaries.
1 | type operand = Int of int | Var of var | Lab of label | Reg of Mips.reg |
Build Control Flow Graph
The second step would be to connect blocks of instructions together, based on the final jump statement in each block.
1 | let make_graph (f: func) : flow_graph = |
There are only two cases that we need to consider, a jump and a if branch. We simply iterate over the list of blocks, which is of type func, and fold the result.
Def and Use Map for each block (Kill and Gen in other words)
This is an optimization to make the convergence of dataflow analysis faster, so that we do not have to recalculate the gen and kill at each iteration.
1 | let process_instruction (def, use) inst = |
Let me just make an important remark on the definition of the gen and kill set of each block. (or def and use in the Appel’s book, which is equivalent). I want to emphasize that because it is not in Appel’s book (Appel did not use the block coalesce optimization, which merge together non jump instructions). Nor does Prof Joe thought of it immediately.
- A def set of a block is the set of variables that are defined before ever used in that block.
- A use set is, on the contrary, the set of variables that are used before ever defined.
So, when doing the def use summary of the block, you start from the bottom, and if you see a variable defined, then you should remove it from the use set, if it exists in the use set.
Liveness Analysis
The liveness analysis algorithm is well established. We just need to implement it in ocaml.
1 | let rec analyze_liveness flow_graph def_use_map live_in_sets live_out_sets = |
What I implemented is just a relatively truthful ocaml version of this algorithm.
Construct Interference Graph
1 | let add_edges_to_igraph (live_in_sets: NodeSet.t BlockMap.t) (live_out_sets: NodeSet.t BlockMap.t): interfere_graph = |
To construct interference graph, we use the live-out set for each block as a starting point, move backward, take out and add in variables to the live set along the way, according to the gen and kill rules. At each instruction step, we add edges between every 2 nodes in the live set, to the interference graph.
Putting everything together
I wrote a main function to put everythong together.
1 | let build_interfere_graph (f : func) = |
[Compiler Construction] Control Flow Graph, Liveness Analysis, Dataflow Analysis and Interference Graph, Implemented in OCaml