Files
revng-revng/scripts/monotone-framework.py
T
Alessandro Di Federico f819962527 Switch to new assertion system globally
This commit enforces on the whole project the usage of our own assertion
system. This means all calls to `abort`, `assert` and `llvm_unreachable`
have been replaced with calls to `revng_abort`, `revng_assert` and
`revng_unreachable`, respectively.

The error messages, usually expressed as `assert(Condition &&
"Message")` have now been replaced using the second (optional) argument
of `revng_assert`.

Additionally, all the `assert(false)` statements have been replaced with
calls to `revng_abort`. Apart from readibility, this ensures the
compilers is aware of the fact that call will never return.

This change will enable us to drop many statements whose sole purpose
was marking a variable employed in an assertion as used in release
mode. In fact, with the new assertion system this is no longer
necessary.
2018-09-29 13:05:16 +02:00

519 lines
13 KiB
Python
Executable File

#!/usr/bin/env python
# This script generates C++ classes from a GraphViz file representing a monotone
# framework.
#
# The graph should have a set of edges with no labels to represent the lattice
# and another set of edges with a label representing the transfer functions.
# Each different label represents a different transfer function.
# Additionally, one of the nodes should have a double border ("peripheries=2")
# to represent it's the initial node.
#
# This script works and should keep working on Python 2.7 and Python 3.
# Standard imports
import sys
import argparse
import re
import tempfile
from itertools import product
from collections import defaultdict
# pygraphviz
from pygraphviz import AGraph
# numpy (optional)
try:
import numpy as np
except ImportError:
np = None
def matrix_multiplication(a, b):
zip_b = list(zip(*b))
return [[sum(a * b for a, b in zip(row, col))
for col in zip_b]
for row in a]
out = lambda string: sys.stdout.write(string)
log = lambda string: sys.stderr.write(string + "\n")
def enumerate_graph(graph):
# Assign an identifier to each node
index = 0
for vertex in graph.nodes_iter():
vertex.attr["index"] = str(index)
index += 1
def get_unique(container):
assert len(container) == 1
return container[0]
def extract_lattice(input_graph):
# Compute the lattice graph from the input graph. In practice we remove all
# the edges that have a label, since they represent a transfer function
return drop_edge_if(input_graph, lambda edge: edge.attr["label"])
def compute_reachability_matrix(graph):
# Build the adjacency matrix
vertices = graph.number_of_nodes()
edges = graph.number_of_edges()
adj = [[float(0) for x in range(vertices)] for y in range(vertices)]
# Self-loop
for x in range(vertices):
adj[x][x] = 1
# Set to 1 all the cells representing a pair of adjacent vertices
for edge in graph.edges_iter():
x = int(edge[0].attr["index"])
y = int(edge[1].attr["index"])
adj[x][y] = 1
# Build the reachability matrix
# TODO: reimplement re-enumerating all the elements in the matrix instead of
# using float
if np is not None:
reachability = (np.matrix(adj) ** edges).tolist()
else:
reachability = adj
for _ in range(edges):
reachability = matrix_multiplication(reachability, adj)
for x in range(vertices):
reachability[x][x] = float("Inf")
return reachability
def is_cyclic(graph, entry):
visited = set()
path = [object()]
path_set = set(path)
stack = [graph.successors_iter(entry)]
while stack:
for v in stack[-1]:
if v in path_set:
return True
elif v not in visited:
visited.add(v)
path.append(v)
path_set.add(v)
stack.append(iter(graph.successors_iter(v)))
break
else:
path_set.remove(path.pop())
stack.pop()
return False
def check_lattice(lattice):
# Check we have a single top and a single bottom
top = None
bottom = None
for v in lattice.nodes_iter():
if lattice.in_degree(v) == 0:
assert bottom == None
bottom = v
if lattice.out_degree(v) == 0:
assert top == None
top = v
assert top is not None
assert bottom is not None
# Check for the absence of back edges
assert not is_cyclic(lattice, bottom)
return top, bottom
def drop_edge_if(graph, condition):
new_graph = graph.copy()
for edge in new_graph.edges():
new_graph.remove_edge(edge)
for edge in graph.edges_iter():
if not condition(edge):
new_graph.add_edge(edge[0], edge[1], None, **edge.attr)
return new_graph
def extract_transfer_function_graph(input_graph, call_arcs):
# Build the transfer function graph. Drop all the edges without a label (those
# representing the lattice).
tf_graph = drop_edge_if(input_graph, lambda edge: not edge.attr["label"])
# Add the ReturnFrom... arcs
if call_arcs:
edge_labels = set(edge.attr["label"] for edge in tf_graph.edges())
for v in tf_graph.nodes_iter():
return_edge_name = "ReturnFrom" + v.name
# Check if the ReturnFrom edge has already been explicitly provided
if return_edge_name in edge_labels:
continue
# All the incoming edges must come from the same source
source = set(edge[0] for edge in tf_graph.in_edges(v))
assert len(source) <= 1
if source:
source = list(source)[0]
tf_graph.add_edge(source, v)
new_edge = tf_graph.get_edge(source, v)
else:
tf_graph.add_edge(v, v)
new_edge = tf_graph.get_edge(v, v)
new_edge.attr["label"] = return_edge_name
return tf_graph
def check_transfer_functions(tf_graph, reachability, transfer_functions):
tf_graph = tf_graph.copy()
# Automatically add self-loops where required
all_vertices = set(tf_graph.nodes())
for name, edges in transfer_functions.iteritems():
no_self = set()
for edge in edges:
no_self.add(edge[0])
need_self = all_vertices - no_self
for vertex in need_self:
tf_graph.add_edge(vertex, vertex)
new_edge = tf_graph.get_edge(vertex, vertex)
new_edge.attr["label"] = name
edges.append(new_edge)
result = True
s = lambda edge: int(edge[0].attr["index"])
d = lambda edge: int(edge[1].attr["index"])
lte = lambda a, b: reachability[a][b] != 0
for name, edges in transfer_functions.items():
for a, b in product(edges, edges):
if a == b:
continue
if lte(s(a), s(b)) and not lte(d(a), d(b)):
result = False
log("The transfer function {} is not monotone:\n".format(name))
log(" {} -> {}\n {} -> {}\n".format(a[0].name,
a[1].name,
b[0].name,
b[1].name))
return result
def process_graph(path, call_arcs):
out = ""
input_graph = AGraph(path)
enumerate_graph(input_graph)
lattice = extract_lattice(input_graph)
# Check that the lattice is valid for a monotone framework
top, bottom = check_lattice(lattice)
reachability = compute_reachability_matrix(lattice)
tf_graph = extract_transfer_function_graph(input_graph, call_arcs)
transfer_functions = defaultdict(lambda: [])
for edge in tf_graph.edges_iter():
transfer_functions[edge.attr["label"]].append(edge)
# Check the monotonicity of the transfer function
assert check_transfer_functions(tf_graph, reachability, transfer_functions)
name = input_graph.name
# Generate C++ class
out += ("""class {} {{
public:
""".format(name))
# Print the enumeration of all the possible lattice values
out += (""" enum Values {
""")
# Get the default lattice element (has the "peripheries" property)
default = [v.name
for v in lattice.nodes_iter()
if v.attr["peripheries"]][0]
# Get all the names
values = sorted([v.name for v in lattice.nodes_iter()])
out += (" " + ",\n ".join(values) + "\n")
out += (""" };
""")
# Print the enumeration of all the possible transfer functions
out += (""" enum TransferFunction {{
""".format())
# Get all the transfer function names
tfs = [e.attr["label"] for e in tf_graph.edges_iter()]
out += (" " + ",\n ".join(sorted(tfs)) + "\n")
out += (""" };
""")
# Emit constructors and factory method for default value (i.e., the initial
# one)
out += ("""public:
{}() :
Value({}) {{ }}
{}(Values V) :
Value(V) {{ }}
static Values initial() {{
return {};
}}
""".format(name, bottom.name, name, default))
# Emit the combine operator
out += (""" void combine(const {} &Other) {{
""".format(name))
node_by_index = lambda index: get_unique([x
for x in lattice.nodes_iter()
if x.attr["index"] == str(index)])
result = defaultdict(lambda: [])
for v1 in lattice.nodes_iter():
for v2 in lattice.nodes_iter():
if v1 != v2:
i1 = int(v1.attr["index"])
i2 = int(v2.attr["index"])
nonzero = lambda i: set([x[0]
for x in enumerate(reachability[i])
if x[1] != 0])
output = max(nonzero(i1) & nonzero(i2),
key=lambda i: reachability[i1][i] + reachability[i2][i])
output = node_by_index(output)
result[output].append((v1, v2))
assert output in result
first = True
for output, pairs in sorted(result.items(), key=lambda x: x[0]):
if first:
out += (""" if (""")
else:
out += (""" else if (""")
conditions = []
for this, other in sorted(pairs, key=lambda x: (x[0].name, x[1].name)):
condition = "(Value == {} && Other.Value == {})"
condition = condition.format(this.name, other.name)
conditions.append(condition)
conditions[0] = conditions[0].lstrip()
conditions_string = ("\n || "
if first
else "\n || ")
out += conditions_string.join(conditions)
out += (""") {{
Value = {};
}}""".format(output.name))
first = False
out += ("""
}
""")
# Emit the comparison operator of the lattice
out += (""" bool greaterThan(const {} &Other) const {{
return !lowerThanOrEqual(Other);
}}
bool lowerThanOrEqual(const {} &Other) const {{
return Value == Other.Value
|| """.format(name, name))
result = []
for v1 in sorted(lattice.nodes_iter(), key=lambda x: x.name):
for v2 in sorted(lattice.nodes_iter(), key=lambda x: x.name):
if v1 != v2:
i1 = int(v1.attr["index"])
i2 = int(v2.attr["index"])
if reachability[i1][i2] != 0:
condition = """(Value == {} && Other.Value == {})"""
condition = condition.format(v1.name, v2.name)
result.append(condition)
out += ("\n || ".join(result))
out += (""";
}
""")
tf_names = sorted([e.attr["label"] for e in tf_graph.edges_iter()])
# Emit the transfer function implementation
out += (""" void transfer(TransferFunction T) {{
switch(T) {{
""".format(name))
for tf in tf_names:
out += (""" case {}:
switch(Value) {{
""".format(tf))
for edge in transfer_functions[tf]:
out += (""" case {}:
Value = {};
break;
""".format(edge[0].name, edge[1].name))
out += (""" default:
break;
}
break;
""")
out += (""" }
}
""")
# Emit the transfer function implementation
out += (""" void transfer(GeneralTransferFunction T) {{
switch(T) {{
""".format(name))
for tf in tf_names:
out += (""" case GeneralTransferFunction::{}:
switch(Value) {{
""".format(tf))
for edge in transfer_functions[tf]:
out += (""" case {}:
Value = {};
break;
""".format(edge[0].name, edge[1].name))
out += (""" default:
break;
}
break;
""")
out += (""" default:
revng_abort();
}
}
""")
if call_arcs:
# Emit the method implementing the transfer function for returning from a
# call
out += (""" TransferFunction returnTransferFunction() const {{
switch(Value) {{
""".format())
for value in values:
out += (""" case {}:
return ReturnFrom{};
""".format(value, value))
out += (""" }
revng_abort();
}
""")
# Emit a method to return the name of analysis
out += (""" static const char *name() {{
return "{}";
}}
""".format(name))
# Emit a method returning the top element
out += (""" static {} top() {{
return {}({});
}}
""".format(name, name, top.name))
# Accessor for the current value
out += (""" Values value() const { return Value; }
""")
# Debug methods
out += (""" void dump() const {{ dump(dbg); }}
template<typename T>
void dump(T &Output) const {{
switch(Value) {{
""".format(name, name, name, top.name))
for value in values:
out += (""" case {}:
Output << "{}";
break;
""".format(value, value))
out += (""" }
}
""")
# Emit private data
out += ("""private:
Values Value;
}};
""".format())
return tf_names, out
def main():
parser = argparse.ArgumentParser(description="Generate C++ code from dot \
files representing a monotone framework.")
parser.add_argument("--call-arcs",
action="store_true",
help="Add call arcs.")
parser.add_argument("header",
metavar="HEADER",
help="C++ file header.")
parser.add_argument("footer",
metavar="FOOTER",
help="C++ file footer.")
parser.add_argument("inputs",
metavar="GRAPH",
nargs="+",
help="GraphViz input file.")
args = parser.parse_args()
# Print the header file
with open(args.header) as header_file:
out(header_file.read())
# Process each input graph
result = ""
all_transfer_functions = set()
for path in args.inputs:
transfer_functions, output = process_graph(path, args.call_arcs)
result += output
all_transfer_functions |= set(transfer_functions)
out("""enum class GeneralTransferFunction {
""")
out(""",
""".join(sorted(all_transfer_functions)))
out(""",
InvalidTransferFunction
};
""")
out(result)
# Print the footer file
with open(args.footer) as footer_file:
out(footer_file.read())
return 0
if __name__ == "__main__":
sys.exit(main())