Skip to content

Instantly share code, notes, and snippets.

@lionello
Last active Jun 14, 2019
Embed
What would you like to do?
Utility to generate plantuml diagram for Fortran project
#!/usr/bin/env python3
import itertools
import sys
import re
import os
from collections import defaultdict
MODULE = re.compile(r'^(END )?MODULE\s+([A-Z0-9_]+)', re.IGNORECASE)
USE = re.compile(r'^USE\s+([A-Z0-9_]+)(\s*,\s*ONLY\s*:\s*([A-Z0-9_,]+))?', re.IGNORECASE)
BEGIN = re.compile(r'^(SUBROUTINE|PROGRAM|((COMPLEX|LOGICAL|INTEGER|REAL|CHARACTER)(\([^)]+\))? )?FUNCTION)\s+([A-Z0-9_]+)', re.IGNORECASE)
CALL = re.compile(r'^\s*CALL\s+([A-Z0-9_]+)', re.IGNORECASE)
END = re.compile(r'^END\s+(SUBROUTINE|PROGRAM|FUNCTION)', re.IGNORECASE)
class Module:
def __init__(self, filename: str, name: str):
self.filename = filename
self.name = name
self.uses = []
self.fns = {} # dictionary from str -> FuncOrSub
def add(self, funcOrSub):
key = (self.name, funcOrSub.name)
assert not key in self.fns
self.fns[key] = funcOrSub
funcOrSub.module = self
def __str__(self):
return "!" + self.filename \
+ f"\nMODULE {self.name}" \
+ "".join("\nUSE " + u for u in self.uses) \
+ "".join("\n" + str(f) for f in self.fns.values()) \
+ f"\nEND MODULE {self.name}"
class FuncOrSub:
def __init__(self, filename: str, typ: str, name: str):
self.filename = filename
self.typ = typ.upper()
self.name = name
self.uses = []
self.calls = []
self.module = None
def __str__(self):
return "!" + self.filename \
+ f"\n{self.typ} {self.name}" \
+ "".join("\n USE " + u for u in self.uses) \
+ "".join("\n CALL " + c for c in self.calls) \
+ f"\nEND {self.typ} {self.name}"
def make_edge(source: str, target: str) -> str:
sourcename = f'"{source}"' if source else '(*)'
return f'{sourcename} --> "{target}"'
def parse_file(filename: str, lazy: bool) -> list:
with open(filename) as file:
modules = set()
unnamed = Module(filename, '')
module = unnamed
funcOrSub = None
modonly = []
funconly = []
prefix = ''
for line in file:
# Remove leading and trailing whitespace
line = line.strip()
# Ignore comments
if line.startswith('!'):
continue
line = prefix + line
if line.endswith('&'):
prefix = line.rstrip(' &')
continue
prefix = '' # reset for next time
# Check whether we're in a MODULE
m = MODULE.match(line)
if m:
modulename = m.group(2)
if m.group(1): # END?
assert module.name == modulename
module = unnamed
modonly = []
else: # start of MODULE
assert module == unnamed
module = Module(filename, modulename)
continue
# Check whether we have any USE declarations
m = USE.match(line)
if m:
modulename = m.group(1)
only = m.group(3)
if funcOrSub:
funcOrSub.uses.append(modulename)
if only:
funconly.extend(only.split(','))
else:
module.uses.append(modulename)
if only:
modonly.extend(only.split(','))
continue
# Check for start of FUNCTION or SUBROUTINE or PROGRAM
m = BEGIN.match(line)
if m:
assert funcOrSub == None
funcOrSub = FuncOrSub(filename, m.group(1), m.group(5))
module.add(funcOrSub)
modules.add(module)
if not lazy and funcOrSub.typ == "PROGRAM":
print(make_edge(None, funcOrSub.name))
continue
# Check for the END FUNCTION or SUBROUTINE or PROGRAM
if END.match(line):
assert funcOrSub != None
funcOrSub = None
funconly = []
continue
# Check for CALL
m = CALL.match(line)
if m:
assert funcOrSub != None
funcOrSub.calls.append(m.group(1))
if not lazy:
print(make_edge(funcOrSub.name, m.group(1)))
continue
# Check for function calls
if modonly or funconly:
for m in re.finditer(r"\b(" + "|".join(modonly + funconly) + ")\\(", line):
assert funcOrSub != None
funcOrSub.calls.append(m.group(1))
return list(modules)
def recurse(source: FuncOrSub, func: FuncOrSub, funcs: dict, edges: set):
edge = (source, func)
if edge in edges:
return
edges.add(edge)
# The modules to try; always try the current module and global module ("")
mods = [func.module.name, ""] + func.uses + func.module.uses
for call in func.calls:
# Find the module with this function
f = None
for mod in mods:
f = funcs.get((mod, call))
if f:
break
if f:
# Recurse into the function we just found
recurse(func, f, funcs, edges)
else:
# Not found; this is likely a builtin function
# tmp = FuncOrSub('builtin', 'SUBROUTINE', call)
# edge = (func, tmp)
# edges.add(edge)
pass
# STEP 1: parse each file and collect all functions
PROGRAM = None
ALL_FUNCS = {} # dictionary that maps tuple -> FuncOrSub
for filename in sys.argv[1:]:
modules = parse_file(filename, lazy=True)
# Add all fns to the funcs dict
for module in modules:
ALL_FUNCS.update(module.fns)
# Check all fns in this file to find the first PROGRAM
for fn in module.fns.values():
if not PROGRAM and fn.typ == "PROGRAM":
PROGRAM = fn
# STEP 2: recurse, starting from the program
EDGES = set()
recurse(None, PROGRAM, ALL_FUNCS, EDGES)
# STEP 3: output
print("@startuml", PROGRAM.name)
# for source,target in edges:
# print(make_edge(source.name if source else None, target))
# STEP 3a: collect all the files and functions that are reachable
FILES = defaultdict(set)
for source,target in EDGES:
if source:
FILES[source.filename].add(source)
FILES[target.filename].add(target)
# STEP 3b: write all the files as packages with funcs
for filename in FILES:
print(f'package "{filename}" {{')
for func in FILES[filename]:
print(f' [{func.name}]')
print('}')
# STEP 3c: write all the edges
print("actor User")
for source,target in EDGES:
print(f'[{source.name if source else "User"}] --> [{target.name}]')
print("@enduml")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment