blob: 3dc0304ea5dcbb85d68f790ed2ddb7656740821d [file] [log] [blame]
# ===--- protocol_graph.py ---------------------------*- coding: utf-8 -*-===//
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
#
# ===---------------------------------------------------------------------===//
#
# Create a graph of the protocol refinement relationships, associated
# types, operator requirements, and defaulted generic operators.
#
# run as follows to view the Nth-largest connected component in a web browser:
#
# N=0 && rm -f /tmp/protocols.dot && \
# python protocol_graph.py stdlib.swift > /tmp/p0.dot && \
# (ccomps -zX#$N -o /tmp/protocols.dot /tmp/p0.dot || true) \
# && dot -Tsvg /tmp/protocols.dot > /tmp/protocols.svg \
# && open /tmp/protocols.svg
#
# ===---------------------------------------------------------------------===//
from __future__ import print_function
import cgi
import os
import re
import sys
# Open 'stdlib.swift' in this directory if no path specified.
args = list(sys.argv) + \
[os.path.join(os.path.dirname(__file__), 'stdlib.swift')]
re_flags = re.MULTILINE | re.VERBOSE
# Pattern to recognize stdlib identifiers (FIXME: doesn't handle Unicode).
identifier = '[A-Za-z_][A-Za-z0-9_]*'
# Pattern to recognize a (possibly-generic) operator decl.
operator = r'''
(?:(?:prefix|postfix).*)? func \s*
(?=\S)[^A-Za-z_] # non-space, non-identifier: begins an operator name
(?:(?=\S)[^(){])* # rest of operator name
\s*
(<[^>{]+>)? # generic parameter list
\s*
\([^)]*\) # function parameter list
'''
# substitute local variables into the string
def interpolate(string):
import inspect
frame = inspect.currentframe()
return string % frame.f_back.f_locals
# Given the body_text of a protocol definition, return a list of
# associated type and operator requirements.
def body_lines(body_text):
return [
cgi.escape(b.group(0)) for b in
re.finditer(
r'(typealias\s*' + identifier +
r'(\s*[:,]\s*' + identifier + ')?|' + operator + '.*)',
body_text, re_flags)
]
# Mapping from protocol to associated type / operator requirements
body = {}
# Mapping from a parent protocol to set of children.
graph = {}
# Mapping from protocol to generic operators taking instances as arguments
generic_operators = {}
# FIXME: doesn't respect strings or comment nesting)
comments = r'//.* | /[*] (.|\n)*? [*]/'
# read source, stripping all comments
with open(args[1]) as src:
source_sans_comments = re.sub(comments, '', src.read(), flags=re_flags)
generic_parameter_constraint = interpolate(
r' (%(identifier)s) \s* : \s* (%(identifier)s) ')
def parse_generic_operator(m):
generic_params = m.group(5)
generic_operator = cgi.escape(m.group(0).strip())
function_param_start = m.end(5) - m.start(0)
function_params = generic_operator[function_param_start:]
for m2 in re.finditer(
generic_parameter_constraint, generic_params, re_flags):
type_parameter = m2.group(1)
protocol = m2.group(2)
# we're only interested if we can find a function parameter of that
# type
if not re.search(r':\s*%s\s*[,)]' % type_parameter, function_params):
continue
# Make some replacements in the signature to limit the graph size
letter_tau = '&#x3c4;'
letter_pi = '&#x3c0;'
abbreviated_signature = re.sub(
r'\b%s\b' % protocol, letter_pi,
re.sub(r'\b%s\b' % type_parameter, letter_tau, generic_operator))
generic_operators.setdefault(
protocol, set()).add(abbreviated_signature)
def parse_protocol(m):
child = m.group(1)
# skip irrelevant protocols
if re.match(r'_Builtin.*Convertible', child):
return
graph.setdefault(child, set())
body[child] = body_lines(m.group(3))
if m.group(2):
for parent in m.group(2).strip().split(","):
if re.match(r'_Builtin.*Convertible', parent):
return
graph.setdefault(parent.strip(), set()).add(child)
protocols_and_operators = interpolate(r'''
\bprotocol \s+ (%(identifier)s) \s*
(?::\s*([^{]+))? # refinements
{([^{}\n]*(.*\n)*?)} # body
|
%(operator)s [^{]*(?={) # operator definition up to the open brace
''')
# Main parsing loop
for m in re.finditer(protocols_and_operators, source_sans_comments, re_flags):
if m.group(1):
parse_protocol(m)
elif m.group(5):
parse_generic_operator(m)
# otherwise we matched some non-generic operator
# Find clusters of protocols that have the same name when underscores
# are stripped
# map from potential cluster name to nodes in the cluster
cluster_builder = {}
for n in graph:
cluster_builder.setdefault(n.translate(None, '_'), set()).add(n)
# Grab the clusters with more than one member.
clusters = dict((c, nodes)
for (c, nodes) in cluster_builder.items() if len(nodes) > 1)
# A set of all intra-cluster edges
cluster_edges = set(
(s, t) for (c, elements) in clusters.items()
for s in elements
for t in graph[s] if t in elements)
print('digraph ProtocolHierarchies {')
# ; packmode="array1"
print(' mclimit = 100; ranksep=1.5; ')
print(' edge [dir="back"];')
print(' node [shape = box, fontname = Helvetica, fontsize = 10];')
for c in sorted(clusters):
print(' subgraph "cluster_%s" {' % c)
for (s, t) in sorted(cluster_edges):
if s in clusters[c]:
print('%s -> %s [weight=100];' % (s, t))
print('}')
for node in sorted(graph.keys()):
requirements = body.get(node, [])
generics = sorted(generic_operators.get(node, set()))
style = 'solid' if node.startswith('_') else 'bold'
divider = '<HR/>\n' if len(requirements) != 0 and len(generics) != 0 \
else ''
label = node if len(requirements + generics) == 0 else (
'\n<TABLE BORDER="0">\n<TR><TD>\n%s\n</TD></TR><HR/>' +
'\n%s%s%s</TABLE>\n' % (
node,
'\n'.join('<TR><TD>%s</TD></TR>' % r for r in requirements),
divider,
'\n'.join('<TR><TD>%s</TD></TR>' % g for g in generics)))
print(interpolate(' %(node)s [style = %(style)s, label=<%(label)s>]'))
for (parent, children) in sorted(graph.items()):
print(' %s -> {' % parent, end=' ')
print('; '.join(sorted(
child for child in children
if not (parent, child) in cluster_edges)
), end=' ')
print('}')
print('}')