Skip to content
86 changes: 60 additions & 26 deletions source/fab/parse/fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,10 @@
Function_Stmt, Language_Binding_Spec, Char_Literal_Constant,
Interface_Block, Name, Comment, Module, Call_Stmt, Derived_Type_Def,
Derived_Type_Stmt, Type_Attr_Spec_List, Type_Attr_Spec, Type_Name,
Subroutine_Subprogram, Function_Subprogram, Internal_Subprogram_Part)
Subroutine_Subprogram, Function_Subprogram, Internal_Subprogram_Part,
External_Stmt, Type_Declaration_Stmt)
from fparser.two.utils import walk # type: ignore

# todo: what else should we be importing from 2008 instead of 2003? This seems fragile.
from fparser.two.Fortran2008 import ( # type: ignore
Type_Declaration_Stmt, Attr_Spec_List)

from fab.build_config import BuildConfig
from fab.dep_tree import AnalysedDependent
from fab.parse.fortran_common import _typed_child, FortranAnalyserBase
Expand Down Expand Up @@ -234,7 +231,9 @@ def walk_nodes(self, fpath, file_hash, node_tree) -> AnalysedFortran:
# Or the new match statement, Python 3.10
if obj_type == Use_Stmt:
self._process_use_statement(analysed_fortran, obj) # raises

elif obj_type == External_Stmt:
for external in obj.items[1].items:
analysed_fortran.add_symbol_dep(external.string)
elif obj_type == Call_Stmt:
called_name = _typed_child(obj, Name)
# called_name will be None for calls like thing%method(),
Expand Down Expand Up @@ -274,10 +273,10 @@ def walk_nodes(self, fpath, file_hash, node_tree) -> AnalysedFortran:
analysed_fortran.add_symbol_dep(called_name.string)

elif obj_type == Program_Stmt:
analysed_fortran.add_program_def(str(obj.get_name()))
analysed_fortran.add_program_def(obj.get_name().string)

elif obj_type == Module_Stmt:
analysed_fortran.add_module_def(str(obj.get_name()))
analysed_fortran.add_module_def(obj.get_name().string)

elif obj_type in (Subroutine_Stmt, Function_Stmt):
self._process_subroutine_or_function(analysed_fortran,
Expand All @@ -289,12 +288,8 @@ def walk_nodes(self, fpath, file_hash, node_tree) -> AnalysedFortran:
# use in C. Variable bindings are bidirectional - does
# this work the other way round, too?
# Make sure we have a test for it.
elif obj_type == Type_Declaration_Stmt:
# bound?
specs = _typed_child(obj, Attr_Spec_List)
if specs and _typed_child(specs, Language_Binding_Spec):
self._process_variable_binding(analysed_fortran, obj)

elif isinstance(obj, Type_Declaration_Stmt):
self._process_type_declaration(analysed_fortran, obj)
elif obj_type == Comment:
self._process_comment(analysed_fortran, obj)

Expand Down Expand Up @@ -341,6 +336,28 @@ def _process_use_statement(self, analysed_file, obj):
# found a dependency on fortran
analysed_file.add_module_dep(use_name)

def _process_type_declaration(self, analysed_fortran, obj):
"""
Handles a type declaration statement. A type declaration symbol
implies an outside dependency if:
1. there is a bind attribute (dependency to other language)
2. external attribute.

Syntax rule:
Type_Declaration_Stmt( ! obj
Intrinsic_Type_Spec ! obj.items[0]
Attr_Spec_List ! obj.items[1]
Entity_Decl_List ! obj.items[2]
"""
attr_spec_list = obj.items[1].items if obj.items[1] else []
for attr in attr_spec_list:
if attr.string == "EXTERNAL":
for symbol in obj.items[2].items:
analysed_fortran.add_symbol_dep(symbol.string)
elif isinstance(attr, Language_Binding_Spec):
# Bind attribute
self._process_variable_binding(analysed_fortran, obj)

def _process_variable_binding(self, analysed_file,
obj: Type_Declaration_Stmt):
# The name keyword on the bind statement is optional.
Expand Down Expand Up @@ -383,7 +400,23 @@ def _process_comment(self, analysed_file, obj):
# Without .o means a Fortran symbol
analysed_file.add_symbol_dep(dep)

def _process_subroutine_or_function(self, analysed_file, fpath, obj):
def _process_subroutine_or_function(
self,
analysed_file: AnalysedFortran,
fpath: Path,
obj: Union[Function_Stmt, Subroutine_Stmt]):
"""
Processes a subroutine statement. It handles:
- a potential 'bind' attribute (which can change the external symbol
used),
- declaration of subroutines/functions in modules (which will not be
visible as external symbols, any dependencies will be covered by
the module symbol)
- declarations contained in other subroutines (which will not
at all be visible)
- declarations in an interface block (which declare an external
dependency)
"""
# binding?
bind = _typed_child(obj, Language_Binding_Spec)
if bind:
Expand All @@ -407,18 +440,19 @@ def _process_subroutine_or_function(self, analysed_file, fpath, obj):
analysed_file.add_symbol_def(bind_name)

# Not bound, just record the presence of the Fortran symbol.
# We don't need to record stuff in modules. Do not record
# any functions/subroutine that are part of a module, contained,
# or an interface block (since these symbols will not be external
# visible, and might otherwise trigger duplicated symbols in Fab)

elif self._find_ancestor(obj, Interface_Block):
# If the subroutine/function declaration is inside an interface
# block, we have an external dependency:
analysed_file.add_symbol_dep(str(obj.get_name()))

elif (not self._find_ancestor(obj, Module) and
not self._find_ancestor(obj, Internal_Subprogram_Part) and
not self._find_ancestor(obj, Interface_Block)):
if isinstance(obj, Subroutine_Stmt):
analysed_file.add_symbol_def(str(obj.get_name()))
elif isinstance(obj, Function_Stmt):
_, name, _, _ = obj.items
analysed_file.add_symbol_def(name.string)
not self._find_ancestor(obj, Internal_Subprogram_Part)):
# We don't need to record stuff in modules, and any functions /
# subroutines that contained in a subroutine either (since these
# will not be externally visible). But otherwise record the
# declaration of the subroutine/function.
analysed_file.add_symbol_def(str(obj.get_name()))


class FortranParserWorkaround():
Expand Down
6 changes: 6 additions & 0 deletions tests/unit_tests/parse/fortran/test_fortran_analyser.f90
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ MODULE foo_mod

SUBROUTINE internal_sub
! DEPENDS ON: monty_func
external some_external_symbol
integer, external :: some_external_as_attribute
INTERFACE
SUBROUTINE sub_in_interface
END SUBROUTINE sub_in_interface
END INTERFACE
RETURN
END SUBROUTINE internal_sub

Expand Down
10 changes: 6 additions & 4 deletions tests/unit_tests/parse/fortran/test_fortran_analyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from unittest import mock

from fparser.common.readfortran import FortranStringReader # type: ignore
from fparser.two.Fortran2008 import Type_Declaration_Stmt # type: ignore
from fparser.two.Fortran2003 import Type_Declaration_Stmt # type: ignore
from fparser.two.parser import ParserFactory # type: ignore
from fparser.two.utils import walk # type: ignore
import pytest
Expand All @@ -37,11 +37,13 @@ def module_expected_fixture(module_fpath: Path) -> AnalysedFortran:
test module.'''
return AnalysedFortran(
fpath=module_fpath,
file_hash=3737289404,
file_hash=3447500859,
module_defs={'foo_mod'},
symbol_defs={'external_sub', 'external_func', 'foo_mod'},
module_deps={'bar_mod', 'compute_chunk_size_mod'},
symbol_deps={'monty_func', 'bar_mod', 'compute_chunk_size_mod'},
symbol_deps={'monty_func', 'bar_mod', 'compute_chunk_size_mod',
'some_external_symbol', 'some_external_as_attribute',
'sub_in_interface'},
file_deps=set(),
mo_commented_file_deps={'some_file.o'},
)
Expand Down Expand Up @@ -156,7 +158,7 @@ def test_program_file(self,
fpath=Path(tmp_file.name))

module_expected.fpath = Path(tmp_file.name)
module_expected._file_hash = 325155675
module_expected._file_hash = 975186955
module_expected.program_defs = {'foo_mod'}
module_expected.module_defs = set()

Expand Down