Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add procedure binding calls to call graph #524

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 80 additions & 32 deletions ford/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import pathlib
import re
from typing import Dict, Iterable, List, Optional, Set, Tuple, Type, Union, cast
from collections import deque
import warnings

from graphviz import Digraph, ExecutableNotFound
Expand Down Expand Up @@ -59,6 +58,7 @@
FortranSourceFile,
FortranSubmodule,
FortranSubmoduleProcedure,
FortranBoundProcedure,
FortranType,
)

Expand Down Expand Up @@ -98,7 +98,7 @@ def is_type(obj):
def is_proc(obj):
return isinstance(
obj,
(FortranProcedure, FortranInterface, FortranSubmoduleProcedure),
(FortranProcedure, FortranInterface, FortranSubmoduleProcedure, FortranBoundProcedure),
)


Expand Down Expand Up @@ -247,6 +247,44 @@ def get_type_node(
return cast(TypeNode, self.get_node(type_, hist))


def get_call_nodes(calls, visited=None, result=None):
'''
takes a list of calls, and returns a set of all the calls that should
be nodes in the graph

not all calls are a node, some are not visible, and some are simple
procedure bindings (bindings that bind one procedure to one label)

these should be skipped, and show a call to their descendant instead
'''

if visited is None:
visited = set()
if result is None:
result = set()

for call in calls:
# ensure we haven't already visited this call
if call in visited:
continue
visited.add(call)

# value for if the call is a simple binding
is_simple_binding = (isinstance(call, FortranBoundProcedure)
and len(call.bindings) == 1
and not isinstance(call.bindings[0], FortranBoundProcedure))

if getattr(call,"visible",True) and not is_simple_binding:
# If the call is visible and isn't a simple binding, add it to the result.
result.add(call)
else:
# If the call is not visible or a simple binding, recursively call the function on the children of the call.
calls = getattr(call, "calls", []) + getattr(call, "bindings", [])
get_call_nodes(calls, visited, result)

return result


class BaseNode:
"""Graph node representing some Fortran entity

Expand Down Expand Up @@ -408,25 +446,33 @@ def __init__(self, obj, gd, hist=None):


class ProcNode(BaseNode):
COLOURS = {"subroutine": "#d9534f", "function": "#d94e8f", "interface": "#A7506F"}
COLOURS = {"subroutine": "#d9534f", "function": "#d94e8f", "interface": "#A7506F", "boundproc": "#A7506F"}

@property
def colour(self):
return ProcNode.COLOURS.get(self.proctype.lower(), super().colour)
return ProcNode.COLOURS.get(self.proctype, super().colour)

def __init__(self, obj, gd, hist=None):
# ToDo: Figure out appropriate way to handle interfaces to routines in submodules.
self.proctype = getattr(obj, "proctype", "")
self.proctype = getattr(obj, "proctype", "").lower()
if self.proctype == "" and isinstance(obj, FortranBoundProcedure):
self.proctype = "boundproc"
super().__init__(obj, gd)

if parent := getattr(obj, "parent", None):
parent_label = f"{parent.name}::"
else:
parent_label = ""
if binding := getattr(obj, "binding", None):
binding_label = f"{binding.parent.name}%"
if isinstance(obj, FortranBoundProcedure):
binder = getattr(obj, "parent", None)
parent = getattr(binder, "parent", None)
else:
binding_label = ""
parent = getattr(obj, "parent", None)
binder = getattr(getattr(obj, "binding", None), "parent", None)

parent_label = ""
binding_label = ""
if parent:
parent_label = f"{parent.name}::"
if binder:
binding_label = f"{binder.name}%"

self.attribs["label"] = f"{parent_label}{binding_label}{self.name}"

self.uses = set()
Expand All @@ -444,20 +490,13 @@ def __init__(self, obj, gd, hist=None):
n = gd.get_module_node(u)
n.used_by.add(self)
self.uses.add(n)

for call in get_call_nodes(getattr(obj, "calls", []) + getattr(obj, "bindings", [])):
n = gd.get_procedure_node(call, hist)
n.called_by.add(self)
self.calls.add(n)

seen = {}
calls = deque(getattr(obj, "calls", []))
while calls:
c = calls.popleft()
if getattr(c, "visible", True):
n = gd.get_procedure_node(c, hist)
n.called_by.add(self)
self.calls.add(n)
elif getattr(c, 'parobj', None) == 'proc' and c not in seen:
calls.extend(getattr(c, "calls", []))
seen[c] = True

if obj.proctype.lower() != "interface":
if self.proctype != "interface":
return

for m in getattr(obj, "modprocs", []):
Expand Down Expand Up @@ -491,10 +530,8 @@ def __init__(self, obj, gd, hist=None):
n.used_by.add(self)
self.uses.add(n)

for c in obj.calls:
if not getattr(c, "visible", False):
continue
n = gd.get_procedure_node(c, {})
for call in get_call_nodes(obj.calls):
n = gd.get_procedure_node(call, hist)
n.called_by.add(self)
self.calls.add(n)

Expand Down Expand Up @@ -1163,7 +1200,10 @@ def _add_node(self, hop_nodes, hop_edges, node, colour):
for p in sorted(node.calls):
if p not in hop_nodes:
hop_nodes.add(p)
hop_edges.append(_solid_edge(node, p, colour))
if getattr(node,"proctype","") != "boundproc":
hop_edges.append(_solid_edge(node, p, colour))
else:
hop_edges.append(_dashed_edge(node, p, colour))
for p in sorted(getattr(node, "interfaces", [])):
if p not in hop_nodes:
hop_nodes.add(p)
Expand All @@ -1185,7 +1225,10 @@ def _add_node(self, hop_nodes, hop_edges, node, colour):
for p in sorted(node.calls):
if p not in self.added:
hop_nodes.add(p)
hop_edges.append(_solid_edge(node, p, colour))
if getattr(node,"proctype","") != "boundproc":
hop_edges.append(_solid_edge(node, p, colour))
else:
hop_edges.append(_dashed_edge(node, p, colour))
for p in sorted(getattr(node, "interfaces", [])):
if p not in self.added:
hop_nodes.add(p)
Expand Down Expand Up @@ -1287,6 +1330,7 @@ def __init__(
self.programs: Set[FortranContainer] = set()
self.procedures: Set[FortranContainer] = set()
self.internal_procedures: Set[FortranContainer] = set()
self.bound_procedures: Set[FortranContainer] = set()
self.types: Set[FortranContainer] = set()
self.sourcefiles: Set[FortranContainer] = set()
self.blockdata: Set[FortranContainer] = set()
Expand Down Expand Up @@ -1315,6 +1359,10 @@ def graph_all(self):
obj.inhergraph = InheritsGraph(obj, self.data)
obj.inherbygraph = InheritedByGraph(obj, self.data)
self.types.add(obj)
# register bound procedures that arn't simple bindings (bindings that bind one procedure to one label)
for bp in getattr(obj,"boundprocs",[]):
if not (len(bp.bindings) == 1 and not isinstance(bp.bindings[0], FortranBoundProcedure)):
self.bound_procedures.add(bp)
elif is_proc(obj):
obj.callsgraph = CallsGraph(obj, self.data)
obj.calledbygraph = CalledByGraph(obj, self.data)
Expand All @@ -1336,7 +1384,7 @@ def graph_all(self):
self.blockdata.add(obj)

usenodes = sorted(list(self.modules))
callnodes = sorted(list(self.procedures | self.internal_procedures))
callnodes = sorted(list(self.procedures | self.internal_procedures | self.bound_procedures))
for p in sorted(self.programs):
if len(p.usesgraph.added) > 1:
usenodes.append(p)
Expand Down
16 changes: 13 additions & 3 deletions ford/sourceform.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,9 +1105,19 @@ def filter_public(collection: dict) -> dict:
and call.name not in getattr(context, "all_types", {})
and call.name not in all_vars
):
# if can't find the call in context, add it as a string
call = context.all_procs.get(call.name, call.name)
tmplst.append(call)
if call.chain == []:
# if can't find the call in context, add it as a string
tmplst.append(context.all_procs.get(call.name, call.name))
# if the call is made from a type, then it must be a bound procedure
else:
for bound in context.boundprocs:
if call.name == bound.name.lower():
tmplst.append(bound)
break
else:
# failed to find the call in context, add it as a string
tmplst.append(call.name)


self.calls = tmplst

Expand Down
27 changes: 27 additions & 0 deletions test/test_graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ def make_project_graphs(tmp_path_factory, request):
contains
procedure :: five
procedure :: six
procedure :: ei => eight
procedure :: ni => nine
generic :: eight_nine => ei, ni
end type alpha

contains
Expand All @@ -85,6 +88,14 @@ def make_project_graphs(tmp_path_factory, request):
call one
end subroutine seven_two
end subroutine seven
subroutine eight(this,x)
class(alpha) :: this
real :: x
end subroutine eight
subroutine nine(this,x)
class(alpha) :: this
integer :: x
end subroutine nine
end module c

submodule (c) c_submod
Expand All @@ -100,6 +111,8 @@ def make_project_graphs(tmp_path_factory, request):
type(alpha) :: y
call y%five()
x = y%six()
call y%ei(1.0)
call y%eight_nine(1.0)
contains
subroutine three
use external_mod
Expand Down Expand Up @@ -214,6 +227,9 @@ def make_project_graphs(tmp_path_factory, request):
"c::alpha%five",
"c::alpha%six",
"c::seven",
"c::alpha%eight",
"c::alpha%nine",
"c::alpha%eight_nine",
],
[
"proc~three->proc~one",
Expand All @@ -225,6 +241,10 @@ def make_project_graphs(tmp_path_factory, request):
"program~foo->proc~five",
"program~foo->proc~six",
"proc~seven->proc~one",
"program~foo->proc~eight",
"program~foo->none~eight_nine",
"none~eight_nine->proc~eight",
"none~eight_nine->proc~nine",
],
PROC_GRAPH_KEY,
),
Expand All @@ -243,6 +263,9 @@ def make_project_graphs(tmp_path_factory, request):
"c::seven",
"seven::seven_one",
"seven::seven_two",
"c::alpha%eight",
"c::alpha%nine",
"c::alpha%eight_nine",
],
[
"proc~three->proc~one",
Expand All @@ -257,6 +280,10 @@ def make_project_graphs(tmp_path_factory, request):
"proc~seven->none~seven_two",
"none~seven_one->none~seven_two",
"none~seven_two->proc~one",
"program~foo->proc~eight",
"program~foo->none~eight_nine",
"none~eight_nine->proc~eight",
"none~eight_nine->proc~nine",
],
PROC_GRAPH_KEY,
),
Expand Down