Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
2022-10-19 10:20:59 +02:00
49 changed files with 2430 additions and 56 deletions
+128
View File
@@ -0,0 +1,128 @@
"""
Utility functions to work with dominators in a :py:class:`CFG <Lib.CFG.CFG>`.
Do not hesitate to look at the source of the functions
to get a better understanding of the algorithms.
"""
from typing import Dict, Set
from graphviz import Digraph
from Lib.CFG import Block, CFG
def computeDom(cfg: CFG) -> Dict[Block, Set[Block]]:
"""
`computeDom(cfg)` computes the table associating blocks to their
dominators in `cfg`.
It works by solving the equation system.
This is an helper function called during SSA entry.
"""
all_blocks: Set[Block] = set(cfg.get_blocks())
dominators: Dict[Block, Set[Block]] = dict()
for b in all_blocks:
if b.get_in(): # If b has some predecessor
dominators[b] = all_blocks
else: # If b has no predecessors
dominators[b] = {b}
new_dominators: Dict[Block, Set[Block]] = dict()
while True:
for b in all_blocks:
if b.get_in():
dom_preds = [dominators[b2] for b2 in b.get_in()]
new_dominators[b] = {b}.union(set.intersection(*dom_preds))
else:
new_dominators[b] = {b}
if dominators == new_dominators:
break
else:
dominators = new_dominators
new_dominators = dict()
return dominators
def printDT(filename: str, graph: Dict[Block, Set[Block]]) -> None: # pragma: no cover
"""Display a graphical rendering of the given domination tree."""
dot = Digraph()
for k in graph:
dot.node(str(k.get_label()))
for k in graph:
for v in graph[k]:
dot.edge(str(k.get_label()), str(v.get_label()))
dot.render(filename, view=True)
def computeDT(cfg: CFG, dominators: Dict[Block, Set[Block]],
dom_graphs: bool, basename: str) -> Dict[Block, Set[Block]]:
"""
`computeDT(cfg, dominators)` computes the domination tree of `cfg`
using the previously computed `dominators`.
It returns `DT`, a dictionary which associates a block with its children
in the dominator tree.
This is an helper function called during SSA entry.
"""
# First, compute the immediate dominators
idominators: Dict[Block, Block] = {}
for b, doms in dominators.items():
# The immediate dominator of b is the unique vertex n ≠ b
# which dominates b and is dominated by all vertices in Dom(b) b.
strict_doms = doms - {b}
idoms = set()
for n in strict_doms:
if strict_doms.issubset(dominators[n]):
idoms.add(n)
if idoms:
assert (len(idoms) == 1)
idominators[b] = idoms.pop()
# Then, simply inverse the relation to obtain the domination tree
DT = {b: set() for b in cfg.get_blocks()}
for i, idominator in idominators.items():
DT[idominator].add(i)
# Print the domination tree if asked
if dom_graphs:
s = "{}.{}.ssa.DT.dot".format(basename, cfg.fdata.get_name())
print("SSA - domination tree graph:", s)
printDT(s, DT)
return DT
def _computeDF_at_block(
cfg: CFG,
dominators: Dict[Block, Set[Block]],
DT: Dict[Block, Set[Block]],
b: Block,
DF: Dict[Block, Set[Block]]) -> None:
"""
`_computeDF_at_block(...)` computes the dominance frontier at the given block,
by updating `DF`.
This is an helper function called during SSA entry.
"""
S: Set[Block] = {succ for succ in cfg.out_blocks(b) if succ not in DT[b]}
for b_succ in DT[b]:
_computeDF_at_block(cfg, dominators, DT, b_succ, DF)
for b_frontier in DF[b_succ]:
if b not in (dominators[b_frontier] - {b_frontier}):
S.add(b_frontier)
DF[b] = S
def computeDF(cfg: CFG, dominators: Dict[Block, Set[Block]],
DT: Dict[Block, Set[Block]], dom_graphs: bool, basename: str
) -> Dict[Block, Set[Block]]:
"""
`computeDF(...)` computes the dominance frontier of a CFG.
It returns `DF` which associates a block to its frontier.
This is an helper function called during SSA entry.
"""
DF: Dict[Block, Set[Block]] = dict()
for b_entry in cfg.get_entries():
_computeDF_at_block(cfg, dominators, DT, b_entry, DF)
# Print the domination frontier on the CFG if asked
if dom_graphs:
s = "{}.{}.ssa.DF.dot".format(basename, cfg.fdata.get_name())
print("SSA - dominance frontier graph:", s)
cfg.print_dot(s, DF, True)
return DF
+306
View File
@@ -0,0 +1,306 @@
""" Python Classes for Oriented and Non Oriented Graphs
"""
from graphviz import Digraph # for dot output
from typing import List, Dict, Set, Tuple, Any
class GraphError(Exception):
"""Exception raised for self loops.
"""
message: str
def __init__(self, message: str):
self.message = message
class GeneralGraph(object):
"""
General class regrouping similarities
between directed and non oriented graphs.
The only differences between the two are:
- how to compute the set of edges
- how to add an edge
- how to print the graph
- how to delete a vertex
- how to delete an edge
- we only color undirected graphs
"""
graph_dict: Dict[Any, Set]
def __init__(self, graph_dict=None):
"""
Initializes a graph object.
If no dictionary or None is given,
an empty dictionary will be used.
"""
if graph_dict is None:
graph_dict = {}
self.graph_dict = graph_dict
def vertices(self) -> List[Any]:
"""Return the vertices of a graph."""
return list(self.graph_dict.keys())
def add_vertex(self, vertex: Any) -> None:
"""
If the vertex "vertex" is not in
self.graph_dict, a key "vertex" with an empty
list as a value is added to the dictionary.
Otherwise nothing has to be done.
"""
if vertex not in self.graph_dict:
self.graph_dict[vertex] = set()
def edges(self) -> List[Set]:
"""Return the edges of the graph."""
return []
def __str__(self):
res = "vertices: "
for k in self.graph_dict:
res += str(k) + " "
res += "\nedges: "
for edge in self.edges():
res += str(edge) + " "
return res
def dfs_traversal(self, root: Any) -> List[Any]:
"""
Compute a depth first search of the graph,
from the vertex root.
"""
seen: List[Any] = []
todo: List[Any] = [root]
while len(todo) > 0: # while todo ...
current = todo.pop()
seen.append(current)
for neighbour in self.graph_dict[current]:
if neighbour not in seen:
todo.append(neighbour)
return seen
def is_reachable_from(self, v1: Any, v2: Any) -> bool:
"""True if there is a path from v1 to v2."""
return v2 in self.dfs_traversal(v1)
def connected_components(self) -> List[List[Any]]:
"""
Compute the list of all connected components of the graph,
each component being a list of vetices.
"""
components: List[List[Any]] = []
done: List[Any] = []
for v in self.vertices():
if v not in done:
v_comp = self.dfs_traversal(v)
components.append(v_comp)
done.extend(v_comp)
return components
def bfs_traversal(self, root: Any) -> List[Any]:
"""
Compute a breadth first search of the graph,
from the vertex root.
"""
seen: List[Any] = []
todo: List[Any] = [root]
while len(todo) > 0: # while todo ...
current = todo.pop(0) # list.pop(0): for dequeuing (on the left...) !
seen.append(current)
for neighbour in self.graph_dict[current]:
if neighbour not in seen:
todo.append(neighbour)
return seen
class Graph(GeneralGraph):
"""Class for non oriented graphs."""
def edges(self) -> List[Set]:
"""
A static method generating the set of edges
(they appear twice in the dictionnary).
Return a list of sets.
"""
edges = []
for vertex in self.graph_dict:
for neighbour in self.graph_dict[vertex]:
if {neighbour, vertex} not in edges:
edges.append({vertex, neighbour})
return edges
def add_edge(self, edge: Tuple[Any, Any]) -> None:
"""
Add an edge in the graph.
edge should be a pair and not (c,c)
(we call g.add_edge((v1,v2)))
"""
(vertex1, vertex2) = edge
if vertex1 == vertex2:
raise GraphError("Cannot add a self loop on vertex {} in an unoriented graph.".format(
str(vertex1)))
if vertex1 in self.graph_dict:
self.graph_dict[vertex1].add(vertex2)
else:
self.graph_dict[vertex1] = {vertex2}
if vertex2 in self.graph_dict:
self.graph_dict[vertex2].add(vertex1)
else:
self.graph_dict[vertex2] = {vertex1}
def print_dot(self, name: str, colors={}) -> None:
"""Print the graph."""
color_names = ['red', 'blue', 'green', 'yellow', 'cyan', 'magenta'] + \
[f"grey{i}" for i in range(0, 100, 10)]
color_shapes = ['ellipse', 'polygon', 'box', 'circle', 'egg', 'pentagon', 'hexagon']
dot = Digraph(comment='Conflict Graph')
for k in self.graph_dict:
shape = None
if not colors:
color = "red" # Graph not colored: red for everyone
elif k not in colors:
color = "grey" # Node not colored: grey
else:
n = colors[k]
if n < len(color_names):
color = color_names[colors[k]]
else:
color = "black" # Too many colors anyway, it won't be readable.
shape = color_shapes[n % len(color_shapes)]
dot.node(str(k), color=color, shape=shape)
for (v1, v2) in self.edges():
dot.edge(str(v1), str(v2), dir="none")
# print(dot.source)
dot.render(name, view=True) # print in pdf
def delete_vertex(self, vertex: Any) -> None:
"""Delete a vertex and all the adjacent edges."""
gdict = self.graph_dict
for neighbour in gdict[vertex]:
gdict[neighbour].remove(vertex)
del gdict[vertex]
def delete_edge(self, edge: Tuple[Any, Any]):
"""Delete an edge."""
(v1, v2) = edge
self.graph_dict[v1].remove(v2)
self.graph_dict[v2].remove(v1)
def color(self) -> Dict[Any, int]:
"""
Color the graph with an unlimited number of colors.
Return a dict vertex -> color, where color is an integer (0, 1, ...).
"""
coloring, _, _ = self.color_with_k_colors()
return coloring
# see algo of the course
def color_with_k_colors(self, K=None, avoidingnodes=()) -> Tuple[Dict[Any, int], bool, List]:
"""
Color with <= K colors (if K is unspecified, use unlimited colors).
Return 3 values:
- a dict vertex -> color
- a Boolean, True if the coloring succeeded
- the set of nodes actually colored
Do not color vertices belonging to avoidingnodes.
Continue even if the algo fails.
"""
if K is None:
K = len(self.graph_dict)
todo_vertices = []
is_total = True
gcopy = Graph(self.graph_dict.copy())
# suppress nodes that are not to be considered.
for node in avoidingnodes:
gcopy.delete_vertex(node)
# append nodes in the list according to their degree and node number:
while gcopy.graph_dict:
todo = list(gcopy.graph_dict)
todo.sort(key=lambda v: (len(gcopy.graph_dict[v]), str(v)))
lower = todo[0]
todo_vertices.append(lower)
gcopy.delete_vertex(lower)
# Now reverse the list: first elements are those with higher degree
# print(todo_vertices)
todo_vertices.reverse() # in place reversal
# print(todo_vertices)
coloring = {}
colored_nodes = []
# gdict will be the coloring map to return
gdict = self.graph_dict
for v in todo_vertices:
seen_neighbours = [x for x in gdict[v] if x in coloring]
choose_among = [i for i in range(K) if not (
i in [coloring[v1] for v1 in seen_neighbours])]
if choose_among:
# if the node can be colored, I choose the minimal color.
color = min(choose_among)
coloring[v] = color
colored_nodes.append(v)
else:
# if I cannot color some node, the coloring is not Total
# but I continue
is_total = False
return (coloring, is_total, colored_nodes)
class DiGraph(GeneralGraph):
"""Class for directed graphs."""
def neighbourhoods(self) -> List[Tuple[Any, Set]]:
"""Return all neighbourhoods in the graph."""
return list(self.graph_dict.items())
def edges(self) -> List[Tuple[Any, Any]]:
""" A static method generating the set of edges"""
edges = []
for vertex in self.graph_dict:
for neighbour in self.graph_dict[vertex]:
edges.append((vertex, neighbour))
return edges
def add_edge(self, edge: Tuple[Any, Any]) -> None:
"""
Add an edge in the graph.
edge should be a pair and not (c,c)
(we call g.add_edge((v1,v2)))
"""
(vertex1, vertex2) = edge
if vertex1 in self.graph_dict:
self.graph_dict[vertex1].add(vertex2)
else:
self.graph_dict[vertex1] = {vertex2}
if vertex2 not in self.graph_dict:
self.graph_dict[vertex2] = set()
def print_dot(self, name: str) -> None:
"""Print the graph."""
dot = Digraph(comment='Conflict Graph')
for k in self.graph_dict:
shape = None
color = "grey"
dot.node(str(k), color=color, shape=shape)
for (v1, v2) in self.edges():
dot.edge(str(v1), str(v2), dir="none")
# print(dot.source)
dot.render(name, view=True) # print in pdf
def delete_vertex(self, vertex: Any) -> None:
"""Delete a vertex and all the adjacent edges."""
for node, neighbours in self.graph_dict.items():
if vertex in neighbours:
neighbours.remove(vertex)
del self.graph_dict[vertex]
def delete_edge(self, edge: Tuple[Any, Any]) -> None:
"""Delete an edge."""
(v1, v2) = edge
self.graph_dict[v1].remove(v2)
+58
View File
@@ -0,0 +1,58 @@
"""
Classes for φ nodes in a RiscV CFG :py:class:`CFG <Lib.CFG.CFG>` under SSA Form:
:py:class:`PhiNode` for a statement of the form temp_x = φ(temp_0, ..., temp_n).
These particular kinds of statements are expected to be in the field
b._phis for a :py:class:`Block <Lib.CFG.Block>` b.
"""
from dataclasses import dataclass
from typing import Dict
from Lib.Operands import Operand, Temporary, DataLocation, Renamer
from Lib.Statement import Statement, Label
@dataclass
class PhiNode(Statement):
"""
A φ node is a renaming in the CFG, of the form temp_x = φ(temp_0, ..., temp_n).
The field var contains the variable temp_x.
The field srcs relies for each precedent block in the CFG, identified with its label,
the variable temp_i of the φ node.
"""
var: DataLocation
srcs: Dict[Label, Operand]
def defined(self):
"""Return the variable defined by the φ node."""
return [self.var]
def used(self) -> Dict[Label, Operand]:
"""
Return the dictionnary associating for each previous block the corresponding variable.
"""
return self.srcs
def rename(self, renamer: Renamer) -> None:
"""Rename the variable defined by the φ node with a fresh name."""
if isinstance(self.var, Temporary):
self.var = renamer.fresh(self.var)
def rename_from(self, renamer: Renamer, label: Label) -> None:
"""Rename the variable associated to the block identified by `label`."""
if label in self.srcs:
t = self.srcs[label]
if isinstance(t, Temporary):
if renamer.defined(t):
self.srcs[label] = renamer.replace(t)
else:
del self.srcs[label]
def __str__(self):
return "{} = φ({})".format(self.var, self.srcs)
def __hash__(self):
return hash((self.var, *self.srcs.items()))
def printIns(self, stream):
print(' # ' + str(self), file=stream)
+20 -19
View File
@@ -5,8 +5,9 @@ Usage:
python3 MiniCC.py --mode <mode> <filename>
python3 MiniCC.py --help
"""
import traceback
from typing import cast
from enum import Enum
from MiniCLexer import MiniCLexer
from MiniCParser import MiniCParser
from TP03.MiniCTypingVisitor import MiniCTypingVisitor, MiniCTypeError
@@ -14,12 +15,11 @@ from TP03.MiniCInterpretVisitor import MiniCInterpretVisitor
from Lib.Errors import (MiniCUnsupportedError, MiniCInternalError,
MiniCRuntimeError, AllocationError)
from enum import Enum
import argparse
from antlr4 import FileStream, CommonTokenStream
from antlr4.error.ErrorListener import ErrorListener
from argparse import ArgumentParser
from traceback import print_exc
import os
import sys
@@ -52,7 +52,7 @@ def valid_modes():
return modes
try:
import TP05.SSA # type: ignore[import]
import TP05.EnterSSA # type: ignore[import]
modes.append('codegen-ssa')
except ImportError:
return modes
@@ -84,7 +84,7 @@ class CountErrorListener(ErrorListener):
def main(inputname, reg_alloc, mode,
typecheck=True, stdout=False, output_name=None, debug=False,
debug_graphs=False, ssa_graphs=False):
debug_graphs=False, ssa_graphs=False, dom_graphs=False):
(basename, rest) = os.path.splitext(inputname)
if mode.is_codegen():
if stdout:
@@ -144,22 +144,19 @@ def main(inputname, reg_alloc, mode,
code = function
else:
from TP04.BuildCFG import build_cfg # type: ignore[import]
from Lib.CFG import CFG # type: ignore[import]
code = build_cfg(function)
assert (isinstance(code, CFG))
if debug_graphs:
s = "{}.{}.dot".format(basename, code.fdata.get_name())
print("CFG:", s)
code.print_dot(s, view=True)
if mode.value >= Mode.SSA.value:
from TP05.SSA import enter_ssa # type: ignore[import]
from TP05.EnterSSA import enter_ssa # type: ignore[import]
from Lib.CFG import CFG # type: ignore[import]
DF = enter_ssa(cast(CFG, code), basename, debug, ssa_graphs)
enter_ssa(cast(CFG, code), dom_graphs, basename)
if ssa_graphs:
s = "{}.{}.ssa.dot".format(basename, code.fdata.get_name())
s = "{}.{}.enterssa.dot".format(basename, code.fdata.get_name())
print("SSA:", s)
code.print_dot(s, DF, True)
code.print_dot(s, view=True)
if mode == Mode.OPTIM:
from TPoptim.OptimSSA import OptimSSA # type: ignore[import]
OptimSSA(cast(CFG, code), debug=debug)
@@ -207,8 +204,8 @@ liveness file not found for {}.".format(form))
allocator.prepare()
if mode.value >= Mode.SSA.value:
from Lib.CFG import CFG # type: ignore[import]
from TP05.SSA import exit_ssa # type: ignore[import]
exit_ssa(cast(CFG, code))
from TP05.ExitSSA import exit_ssa # type: ignore[import]
exit_ssa(cast(CFG, code), reg_alloc == 'smart')
comment += " with SSA"
if allocator:
allocator.rewriteCode(code)
@@ -233,7 +230,7 @@ if __name__ == '__main__':
modes = valid_modes()
parser = argparse.ArgumentParser(description='Generate code for .c file')
parser = ArgumentParser(description='CAP/MIF08 MiniCC compiler')
parser.add_argument('filename', type=str,
help='Source file.')
@@ -265,7 +262,10 @@ if __name__ == '__main__':
if "codegen-ssa" in modes:
parser.add_argument('--ssa-graphs', action='store_true',
default=False,
help='Display SSA graphs (DT, DF).')
help='Display the CFG at SSA entry and exit.')
parser.add_argument('--dom-graphs', action='store_true',
default=False,
help='Display dominance-related graphs (DT, DF).')
args = parser.parse_args()
reg_alloc = args.reg_alloc if "codegen-linear" in modes else None
@@ -273,6 +273,7 @@ if __name__ == '__main__':
outfile = args.output if "codegen-linear" in modes else None
graphs = args.graphs if "codegen-cfg" in modes else False
ssa_graphs = args.ssa_graphs if "codegen-ssa" in modes else False
dom_graphs = args.dom_graphs if "codegen-ssa" in modes else False
if reg_alloc is None and "codegen" in args.mode:
print("error: the following arguments is required: --reg-alloc")
@@ -308,10 +309,10 @@ if __name__ == '__main__':
main(args.filename, reg_alloc, mode,
typecheck,
to_stdout, outfile, args.debug,
graphs, ssa_graphs)
graphs, ssa_graphs, dom_graphs)
except MiniCUnsupportedError as e:
print(e)
exit(5)
except (MiniCInternalError, AllocationError):
traceback.print_exc()
print_exc()
exit(4)
+20
View File
@@ -0,0 +1,20 @@
# MiniC Compiler
LAB5a (Control Flow Graph in SSA Form) & LAB5b (Smart Register Allocation), CAP 2022-23
# Authors
YOUR NAME HERE
# Contents
TODO:
- Explain any design choices you may have made.
- Do not forget to remove all debug traces from your code!
# Test design
TODO: give the main objectives of your tests.
# Known bugs
TODO: bugs you could not fix (if any).
+70
View File
@@ -0,0 +1,70 @@
"""
CAP, SSA Intro, Elimination and Optimisations
Functions to convert a CFG into SSA Form.
"""
from typing import List, Dict, Set
from Lib.CFG import Block, CFG
from Lib.Operands import Renamer
from Lib.Statement import Instruction
from Lib.PhiNode import PhiNode
from Lib.Dominators import computeDom, computeDT, computeDF
def insertPhis(cfg: CFG, DF: Dict[Block, Set[Block]]) -> None:
"""
`insertPhis(CFG, DF)` inserts phi nodes in `cfg` where needed.
At this point, phi nodes will look like `temp_x = φ(temp_x, ..., temp_x)`.
This is an helper function called during SSA entry.
"""
for var, defs in cfg.gather_defs().items():
has_phi: Set[Block] = set()
queue: List[Block] = list(defs)
while queue:
d = queue.pop(0)
for b in DF[d]:
if b not in has_phi:
# TODO add a phi node in block `b` (Lab 5a, Exercise 4)
raise NotImplementedError("insertPhis")
def rename_block(cfg: CFG, DT: Dict[Block, Set[Block]], renamer: Renamer, b: Block) -> None:
"""
Rename variables from block b.
This is an auxiliary function for `rename_variables`.
"""
renamer = renamer.copy()
for i in b.get_all_statements():
if isinstance(i, Instruction | PhiNode):
i.rename(renamer)
for succ in cfg.out_blocks(b):
for i in succ._phis:
assert (isinstance(i, PhiNode))
i.rename_from(renamer, b.get_label())
# TODO recursive call(s) of rename_block (Lab 5a, Exercise 5)
def rename_variables(cfg: CFG, DT: Dict[Block, Set[Block]]) -> None:
"""
Rename variables in the CFG, to transform `temp_x = φ(temp_x, ..., temp_x)`
into `temp_x = φ(temp_0, ... temp_n)`.
This is an helper function called during SSA entry.
"""
renamer = Renamer(cfg.fdata._pool)
# TODO initial call(s) to rename_block (Lab 5a, Exercise 5)
def enter_ssa(cfg: CFG, dom_graphs=False, basename="prog") -> None:
"""
Convert the CFG `cfg` into SSA Form:
compute the dominance frontier, then insert phi nodes and finally
rename variables accordingly.
`dom_graphs` indicates if we have to print the domination graphs.
`basename` is used for the names of the produced graphs.
"""
# TODO implement this function (Lab 5a, Exercise 2)
raise NotImplementedError("enter_ssa")
+48
View File
@@ -0,0 +1,48 @@
"""
CAP, SSA Intro, Elimination and Optimisations
Functions to convert a CFG out of SSA Form.
"""
from typing import cast, List, Set, Tuple
from Lib import RiscV
from Lib.Graphes import DiGraph
from Lib.CFG import Block, BlockInstr, CFG
from Lib.Operands import (
Register, DataLocation,
Temporary)
from Lib.Statement import AbsoluteJump
from Lib.Terminator import BranchingTerminator, Return
from Lib.PhiNode import PhiNode
def generate_moves_from_phis(phis: List[PhiNode], parent: Block) -> List[BlockInstr]:
"""
`generate_moves_from_phis(phis, parent)` builds a list of move instructions
to be inserted in a new block between `parent` and the block with phi nodes
`phis`.
This is an helper function called during SSA exit.
"""
moves: List[BlockInstr] = []
# TODO compute 'moves', a list of 'mv' instructions to insert under parent
# (Lab 5a, Exercise 6)
return moves
def exit_ssa(cfg: CFG, is_smart: bool) -> None:
"""
`exit_ssa(cfg)` replaces phi nodes with move instructions to exit SSA form.
`is_smart` is set to true when smart register allocation is enabled (Lab 5b).
"""
for b in cfg.get_blocks():
phis = cast(List[PhiNode], b._phis) # Use cast for Pyright
b._phis = [] # Remove all phi nodes in the block
parents: List[Block] = b.get_in().copy() # Copy as we modify it by adding blocks
for parent in parents:
moves = generate_moves_from_phis(phis, parent)
# TODO Add the block containing 'moves' to 'cfg'
# and update edges and jumps accordingly (Lab 5a, Exercise 6)
raise NotImplementedError("exit_ssa")
+1
View File
@@ -0,0 +1 @@
Add your own tests in this directory.
Binary file not shown.