mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
fix(fortran): extract named generic interfaces as callable nodes
A named generic interface (`interface area; module procedure area_circle, area_square; end interface`) names a callable that dispatches to its specific procedures by argument type. The interface block was never turned into a node, so a call to the generic name dangled — a Fortran function call_expression only resolves to an in-file node — and the dispatch set was invisible, even though the specific procedures were extracted. Generic interfaces (and operator overload sets) are routine in scientific Fortran. Emit a node for the generic name, link it to the procedures it groups with `dispatches_to` edges (resolved after the walk, like type-bound procedures, so a procedure from another module becomes a collapsible sourceless stub), and the existing call resolution then binds `area(x)` to it. Unnamed abstract/explicit interfaces keep recursing so their procedure declarations are still extracted.
This commit is contained in:
1 parent
5c7b84792f
commit
ccfb4018fb
2 files changed
+91
No files matched your search
@@ -131,6 +131,10 @@ def extract_fortran(path: Path) -> dict:
|
||||
# procedure. Resolved after the whole tree is walked, once the module's
|
||||
# internal procedures have their nodes.
|
||||
type_bound_procs: list[tuple[str, str, int]] = []
|
||||
# (generic_nid, specific_procedure_name, line) for each procedure grouped
|
||||
# under a named generic interface. Resolved after the walk, like the
|
||||
# type-bound procedures above.
|
||||
generic_procs: list[tuple[str, str, int]] = []
|
||||
|
||||
def _fortran_name(stmt_node) -> str | None:
|
||||
"""Extract name from a *_statement node. Fortran is case-insensitive; lowercase."""
|
||||
@@ -345,6 +349,42 @@ def extract_fortran(path: Path) -> dict:
|
||||
walk(child, nid)
|
||||
return
|
||||
|
||||
if t == "interface":
|
||||
stmt = next((c for c in node.children
|
||||
if c.type == "interface_statement"), None)
|
||||
name_node = next((c for c in stmt.children if c.type == "name"),
|
||||
None) if stmt is not None else None
|
||||
if name_node is not None:
|
||||
# Named generic interface (`interface area; module procedure
|
||||
# area_circle, area_square; end interface`): `area` is a callable
|
||||
# generic name that dispatches to its specific procedures by
|
||||
# argument type. It was never a node, so every `area(...)` call
|
||||
# dangled — a function call_expression only resolves to an
|
||||
# in-file node. Emit the generic node and link it to the
|
||||
# procedures it groups, so calls resolve and the dispatch set is
|
||||
# visible.
|
||||
gen_name = _read_text(name_node, source).lower()
|
||||
gen_nid = _make_id(stem, gen_name)
|
||||
line = node.start_point[0] + 1
|
||||
add_node(gen_nid, f"{gen_name}()", line)
|
||||
add_edge(scope_nid, gen_nid, "defines", line)
|
||||
for proc_stmt in node.children:
|
||||
if proc_stmt.type != "procedure_statement":
|
||||
continue
|
||||
for mn in proc_stmt.children:
|
||||
if mn.type == "method_name":
|
||||
generic_procs.append((
|
||||
gen_nid,
|
||||
_read_text(mn, source).lower(),
|
||||
proc_stmt.start_point[0] + 1,
|
||||
))
|
||||
return
|
||||
# Unnamed abstract/explicit interface: recurse so the procedure
|
||||
# declarations it holds still get nodes (unchanged behaviour).
|
||||
for child in node.children:
|
||||
walk(child, scope_nid)
|
||||
return
|
||||
|
||||
if t == "use_statement":
|
||||
line = node.start_point[0] + 1
|
||||
# tree-sitter-fortran uses module_name node for the used module
|
||||
@@ -370,6 +410,15 @@ def extract_fortran(path: Path) -> dict:
|
||||
if tgt != type_nid:
|
||||
add_edge(type_nid, tgt, "method", line, context="type_bound_procedure")
|
||||
|
||||
# Link each generic interface to the specific procedures it groups. A
|
||||
# procedure imported from another module resolves to a sourceless stub the
|
||||
# corpus rewire can collapse (same treatment as type-bound procedures).
|
||||
for gen_nid, impl_name, line in generic_procs:
|
||||
tgt = ensure_named_node(impl_name, line)
|
||||
if tgt != gen_nid:
|
||||
add_edge(gen_nid, tgt, "dispatches_to", line,
|
||||
context="generic_interface")
|
||||
|
||||
_stmt_headers = {
|
||||
"subroutine_statement", "function_statement",
|
||||
"program_statement", "module_statement",
|
||||
|
||||
@@ -2626,6 +2626,48 @@ def test_fortran_type_bound_procedures_link_to_the_type(tmp_path):
|
||||
assert ("circle", "scale") in methods
|
||||
|
||||
|
||||
def test_fortran_generic_interface_is_a_callable_node(tmp_path):
|
||||
"""A named generic interface (`interface area; module procedure …`) names a
|
||||
callable that dispatches to its specific procedures.
|
||||
|
||||
The interface was never a node, so a call to the generic name dangled (a
|
||||
function call only resolves to an in-file node) and the dispatch set was
|
||||
invisible — even though the specific procedures were extracted. The generic
|
||||
name must be a node, linked to the procedures it groups, so `area(x)`
|
||||
resolves.
|
||||
"""
|
||||
src = tmp_path / "shapes.f90"
|
||||
src.write_text(
|
||||
"module shapes\n"
|
||||
" interface area\n"
|
||||
" module procedure area_circle, area_square\n"
|
||||
" end interface\n"
|
||||
"contains\n"
|
||||
" real function area_circle(r)\n"
|
||||
" real :: r\n"
|
||||
" area_circle = r\n"
|
||||
" end function\n"
|
||||
" real function area_square(s)\n"
|
||||
" real :: s\n"
|
||||
" area_square = s\n"
|
||||
" end function\n"
|
||||
" subroutine report(x)\n"
|
||||
" real :: x, a\n"
|
||||
" a = area(x)\n"
|
||||
" end subroutine\n"
|
||||
"end module\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
r = extract_fortran(src)
|
||||
assert "error" not in r
|
||||
assert "area()" in [n["label"] for n in r["nodes"]], "generic interface dropped"
|
||||
dispatch = _edge_labels(r, "dispatches_to", "generic_interface")
|
||||
assert ("area", "area_circle") in dispatch
|
||||
assert ("area", "area_square") in dispatch
|
||||
# A call to the generic name now resolves instead of dangling.
|
||||
assert ("report", "area") in _edge_labels(r, "calls")
|
||||
|
||||
|
||||
def test_fortran_type_bound_procedure_from_other_module_is_sourceless(tmp_path):
|
||||
"""A binding to a procedure implemented in another module resolves to a
|
||||
sourceless stub the corpus rewire can collapse, never a dangling edge."""
|
||||
|
||||
Reference in new issue
Block a user