#!/usr/bin/env python2
import re
import collections
import copy
# graph ::= "digraph" characters '{' statements '}'
# statements ::= '' | statement statements
# statement ::= label | edge
# label ::= characters labelname
# labelname ::= '[' "label" '=' quotedstring ']' ";"
# quotedstring ::= '"' labelstring '"'
# labelstring ::= characters | ' ' labelstring | characters labelstring
# edge ::= characters "->" characters ";"
# characters ::= '' | 'a-zA-Z0-9_-' characters
class Token(object):
EQUALS = 0
DIGRAPH = 1
IDENTIFIER = 2
QUOTE = 3
OPEN_CURLY = 4
CLOSE_CURLY = 5
OPEN_SQUARE = 6
CLOSE_SQUARE = 7
LABEL = 8
SEMICOLON = 9
ARROW = 10
tokens = [(re.compile('='), Token.EQUALS),
(re.compile('[ \n\t]+'), None),
(re.compile('digraph'), Token.DIGRAPH),
(re.compile('\".*\"'), Token.QUOTE),
(re.compile('{'), Token.OPEN_CURLY),
(re.compile('}'), Token.CLOSE_CURLY),
(re.compile('\['), Token.OPEN_SQUARE),
(re.compile(']'), Token.CLOSE_SQUARE),
(re.compile('label'), Token.LABEL),
(re.compile(';'), Token.SEMICOLON),
(re.compile('[a-zA-Z0-9_]+'), Token.IDENTIFIER),
(re.compile('->'), Token.ARROW)]
lexer_output = list()
characters = open('/dev/stdin', 'r').read()
pos = 0
while pos < len(characters):
match = None
for reg, token in tokens:
match = reg.match(characters[pos:])
if match:
if token is not None:
lexer_output.append((token, match.group(0)))
length = len(match.group(0))
assert length > 0, characters[pos:(pos + 20)]
pos += length
break
else:
raise RuntimeError('Unable to lex: "%s"...' % (characters[pos:(pos + 20)]))
class Graph(object):
def __init__(self, name):
self.name = name
self.statements = list()
def add_statement(self, s):
self.statements.append(s)
def __str__(self):
ret = 'digraph %s {\n' % self.name
for s in self.statements:
ret += str(s) + '\n'
ret += '}\n'
return ret
def simplify(self):
# return true if to_node is a child of from_node
def is_reachable(from_node, to_node):
for child in dep[from_node]:
if child == to_node:
return True
if is_reachable(child, to_node):
return True
return False
dep = collections.defaultdict(list)
for s in self.statements:
if isinstance(s, Edge):
dep[s.n1].append(s.n2)
# remove redundant edges
# an edge from a to b is redundant if a connection from a child of a to b is found
to_remove = list()
for parent_node, my_edges in dep.items():
for direct_child in copy.copy(my_edges):
to_visit = copy.copy(dep[direct_child])
while to_visit:
indirect_child = to_visit.pop()
to_visit.extend(dep[indirect_child])
if is_reachable(direct_child, indirect_child) and indirect_child in dep[parent_node]:
to_remove.append((parent_node, indirect_child))
for dependency, this_node in to_remove:
self.statements = [s for s in self.statements if not (isinstance(s, Edge) and s.n1 == dependency and s.n2 == this_node)]
class Label(object):
def __init__(self, name, label):
self.name = name
self.label = label
def __str__(self):
return ' %s [label=%s];' % (self.name, self.label)
class Edge(object):
def __init__(self, n1, n2):
self.n1 = n1
self.n2 = n2
def __str__(self):
return ' %s -> %s;' % (self.n1, self.n2)
class Parser(object):
def __init__(self, lex):
self.lex = lex
self.pos = 0
self.token = self.lex[self.pos]
def next(self):
self.pos += 1
if self.pos < len(self.lex):
self.token = self.lex[self.pos]
def parse(self):
self.match(Token.DIGRAPH)
name = self.identifier()
self.graph = Graph(name)
self.match(Token.OPEN_CURLY)
self.statements()
self.match(Token.CLOSE_CURLY)
def identifier(self):
if self.token[0] != Token.IDENTIFIER:
raise RuntimeError('Expected identifier, received "%s"' % self.token[0])
ret = self.token[1]
self.next()
return ret
def quote(self):
if self.token[0] != Token.QUOTE:
raise RuntimeError('Expected quote, received "%s"' % self.token[0])
ret = self.token[1]
self.next()
return ret
def match(self, char):
if self.token[0] != char:
raise RuntimeError('Expected "%s", received "%s"' % (char, self.token))
self.next()
def statements(self):
while self.token[0] != Token.CLOSE_CURLY:
name = self.identifier()
if self.token[0] == Token.OPEN_SQUARE:
self.match(Token.OPEN_SQUARE)
self.match(Token.LABEL)
self.match(Token.EQUALS)
label = self.quote()
self.match(Token.CLOSE_SQUARE)
self.match(Token.SEMICOLON)
self.graph.add_statement(Label(name, label))
elif self.token[0] == Token.ARROW:
self.match(Token.ARROW)
name2 = self.identifier()
self.match(Token.SEMICOLON)
self.graph.add_statement(Edge(name, name2))
else:
raise RuntimeError('Expected statement, received "%s"' % self.token)
p = Parser(lexer_output)
p.parse()
p.graph.simplify()
print p.graph