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

Utility transformation for creating standalone subroutines from contained subroutines #181

Merged
merged 22 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
aa58ccd
added tests and lift_contained_subroutines function, some tests fail …
skarppinen Oct 24, 2023
2937c12
added a test about undefined globals in parent. all tests pass
skarppinen Oct 25, 2023
1d89e78
insert parent to the beginning of returned routines instead. strip ro…
skarppinen Oct 25, 2023
79d9751
added docs to function
skarppinen Oct 25, 2023
a0cdfdb
added copyright header to files
skarppinen Nov 24, 2023
2b5529f
fix formatting of function docs
skarppinen Nov 24, 2023
d7563d7
rename operation to extract_contained_subroutines, fix all tests, int…
skarppinen Nov 24, 2023
4a6a397
raise runtimeerror instead of generic exception, remove old implement…
skarppinen Nov 24, 2023
9c33477
explicitly load imports from submodules of loki
skarppinen Nov 24, 2023
84a8e97
..also in tests
skarppinen Nov 24, 2023
07aed1b
use import_map instead of own function
skarppinen Nov 24, 2023
0b833ee
remove unnecessary clones
skarppinen Nov 24, 2023
a7cb1b1
add support for processing contained functions as well. fix naming ap…
skarppinen Nov 24, 2023
6352cb2
modify docs to reflect changes
skarppinen Nov 24, 2023
0c78922
fix linter warnings and modify AUTHORS
skarppinen Nov 24, 2023
ca306ab
fix typo in docstring
skarppinen Nov 24, 2023
22fc3d6
simplify variable resolution greatly. instead of strings, use lokis v…
skarppinen Nov 28, 2023
4adcf00
Merge branch 'main' into lift-member-routines
reuterbal Nov 29, 2023
c4c5e5a
lots of small fixes, refactoring and new tests
skarppinen Nov 30, 2023
cf6ced3
Merge branch 'lift-member-routines' of github.com:skarppinen/loki int…
skarppinen Nov 30, 2023
996e9fe
style tweaks for pep8, added one xfail to tests because of likely fro…
skarppinen Dec 4, 2023
fa1dd79
fix one test assertion for OMNI frontend
skarppinen Dec 5, 2023
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
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Authors and Contributors

- R. Heilemann Myhre (Met Norway)
- S. Karppinen (FMI)
- P. Kiepas (École polytechnique/IPSL)
- M. Lange (ECMWF)
- J. Legaux (CERFACS)
Expand Down
1 change: 1 addition & 0 deletions loki/transform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@
from loki.transform.build_system_transform import * # noqa
from loki.transform.transform_hoist_variables import * # noqa
from loki.transform.transform_parametrise import * # noqa
from loki.transform.transform_extract_contained_procedures import * # noqa
from loki.transform.transform_sequence_association import * # noqa
198 changes: 198 additions & 0 deletions loki/transform/transform_extract_contained_procedures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# (C) Copyright 2018- ECMWF.
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.

from loki.subroutine import Subroutine
from loki.expression import (
FindVariables, FindInlineCalls, SubstituteExpressions,
DeferredTypeSymbol, Array
)
from loki.ir import (
CallStatement, DerivedType
)
from loki.visitors import (
Transformer, FindNodes,
)
__all__ = ['extract_contained_procedures', 'extract_contained_procedure']

skarppinen marked this conversation as resolved.
Show resolved Hide resolved
def extract_contained_procedures(procedure):
"""
This transform creates "standalone" :any:`Subroutine`s
from the contained procedures (subroutines or functions) of ``procedure``.

A list of :any:`Subroutine`s corresponding to each contained subroutine of
``procedure`` is returned and ``procedure`` itself is
modified (see below).
This function does the following transforms:
1. all global bindings from the point of view of the contained procedures(s) are introduced
as imports or dummy arguments to the modified contained procedures(s) to make them standalone.
2. all calls or invocations of the contained procedures in parent are modified accordingly.
3. All procedures are removed from the CONTAINS block of ``procedure``.

As a basic example of this transformation, the Fortran subroutine:
.. code-block::
subroutine outer()
integer :: y
integer :: o
o = 0
y = 1
call inner(o)
contains
subroutine inner(o)
integer, intent(inout) :: o
integer :: x
x = 4
o = x + y ! Note, 'y' is "global" here!
end subroutine inner
end subroutine outer
is modified to:
.. code-block::
subroutine outer()
integer :: y
integer :: o
o = 0
y = 1
call inner(o, y) ! 'y' now passed as argument.
contains
end subroutine outer
and the (modified) child:
.. code-block::
subroutine inner(o, y)
integer, intent(inout) :: o
integer, intent(inout) :: y
integer :: x
x = 4
o = x + y ! Note, 'y' is no longer "global"
end subroutine inner
is returned.
"""
new_procedures = []
for r in procedure.subroutines:
new_procedures += [extract_contained_procedure(procedure, r.name)]

# Remove all subroutines (or functions) from the CONTAINS section.
newbody = tuple(r for r in procedure.contains.body if not isinstance(r, Subroutine))
procedure.contains = procedure.contains.clone(body=newbody)
return new_procedures

def extract_contained_procedure(procedure, name):
"""
Extract a single contained procedure with name ``name`` from the parent procedure ``procedure``.

This function does the following transforms:
1. all global bindings from the point of view of the contained procedure are introduced
as imports or dummy arguments to the modified contained procedure returned from this function.
2. all calls or invocations of the contained procedure in the parent are modified accordingly.

See also the "driver" function ``extract_contained_procedures``, which applies this function to each
contained procedure of a parent procedure and additionally empties the CONTAINS section of subroutines.
"""
inner = procedure.subroutine_map[name] # Fetch the subprocedure to extract (or crash with 'KeyError').

# Check if there are variables that don't have a scope. This means that they are not defined anywhere
# and execution cannot continue.
undefined = tuple(v for v in FindVariables().visit(inner.body) if not v.scope)
if undefined:
msg = f"The following variables appearing in the contained procedure '{inner.name}' are undefined "
msg += f"in both '{inner.name}' and the parent procedure '{procedure.name}': "
for u in undefined:
msg += f"{u.name}, "
raise RuntimeError(msg)

## PRODUCING VARIABLES TO INTRODUCE AS DUMMY ARGUMENTS TO `inner`.
# Produce a list of variables defined in the scope of `procedure` that need to be resolved in `inner`'s scope
# by introducing them as dummy arguments.
# The second line drops any derived type fields, don't want them, since want to resolve the derived type itself.
vars_to_resolve = [v for v in FindVariables().visit(inner.body) if v.scope is procedure]
vars_to_resolve = [v for v in vars_to_resolve if not v.parent]

# Save any `DeferredTypeSymbol`s for later, they are in fact defined through imports in `procedure`,
# and therefore not to be added as arguments to `inner`. (the next step removes them from `vars_to_resolve`)
var_imports_to_add = tuple(v for v in vars_to_resolve if isinstance(v, DeferredTypeSymbol))

# Lookup the definition of the variables in `vars_to_resolve` from the scope of `procedure`.
# This provides maximal information on them.
vars_to_resolve = [proc_var for v in vars_to_resolve if \
(proc_var := procedure.variable_map.get(v.name))]

# For each array in `vars_to_resolve`, append any non-literal shape variables to `vars_to_resolve`,
# if not already there.
arr_shapes = []
for var in vars_to_resolve:
if isinstance(var, Array):
# Dropping variables with parents here to handle the case that the array dimension(s)
# are defined through the field of a derived type.
arr_shapes += list(v for v in FindVariables().visit(var.shape) if not v.parent)
for v in arr_shapes:
if v.name not in vars_to_resolve:
vars_to_resolve.append(v)
vars_to_resolve = tuple(vars_to_resolve)

## PRODUCING IMPORTS TO INTRODUCE TO `inner`.
# Get all variables from `inner.spec`. Need to search them for resolving kinds and derived types for
# variables that do not need resolution.
inner_spec_vars = tuple(FindVariables().visit(inner.spec))

# Produce derived types appearing in `vars_to_resolve` or in `inner.spec` that need to be resolved
# from imports of `procedure`.
dtype_imports_to_add = tuple(v.type.dtype for v in vars_to_resolve + inner_spec_vars \
if isinstance(v.type.dtype, DerivedType))

# Produce kinds appearing in `vars_to_resolve` or in `inner.spec` that need to be resolved
# from imports of `procedure`.
kind_imports_to_add = tuple(v.type.kind for v in vars_to_resolve + inner_spec_vars \
if v.type.kind and v.type.kind.scope is procedure)

# Produce all imports to add.
# Here the imports are also tidied to only import what is strictly necessary, and with single
# USE statements for each module.
imports_to_add = []
to_lookup_from_imports = dtype_imports_to_add + kind_imports_to_add + var_imports_to_add
for val in to_lookup_from_imports:
imp = procedure.import_map[val.name]
matching_import = tuple(i for i in imports_to_add if i.module == imp.module)
if matching_import:
# Have already encountered module name, modify existing.
matching_import = matching_import[0]
imports_to_add.remove(matching_import)
newimport = matching_import.clone(symbols=tuple(set(matching_import.symbols + imp.symbols)))
else:
# Have not encountered the module yet, add new one.
newsyms = tuple(s for s in imp.symbols if s.name == val.name)
newimport = imp.clone(symbols=newsyms)
imports_to_add.append(newimport)

## MAKING THE CHANGES TO `inner`
# Change `inner` to take `vars_to_resolve` as dummy arguments and add all necessary imports.
# Here also rescoping all variables to the scope of `inner` and specifying intent as "inout",
# if not set in `procedure` scope.
# Note: After these lines, `inner` should be self-contained or there is a bug.
inner.arguments += tuple(
v.clone(type=v.type.clone(intent=v.type.intent or 'inout'), scope=inner)
for v in vars_to_resolve
)
inner.spec.prepend(imports_to_add)

## TRANSFORMING CALLS TO `inner` in `procedure`.
# The resolved variables are all added as keyword arguments to each call.
# (to avoid further modification of the call if it already happens to contain kwargs).
# Here any dimensions in the variables are dropped, since they should not appear in the call.
# Note that functions need different visitors and mappers than subroutines.
call_map = {}
if inner.is_function:
for call in FindInlineCalls().visit(procedure.body):
if call.routine is inner:
newkwargs = tuple((v.name, v.clone(dimensions=None, scope=procedure)) for v in vars_to_resolve)
call_map[call] = call.clone(kw_parameters=call.kwarguments + newkwargs)
procedure.body = SubstituteExpressions(call_map).visit(procedure.body)
else:
for call in FindNodes(CallStatement).visit(procedure.body):
if call.routine is inner:
newkwargs = tuple((v.name, v.clone(dimensions=None, scope=procedure)) for v in vars_to_resolve)
call_map[call] = call.clone(kwarguments=tuple(call.kwarguments) + newkwargs)
procedure.body = Transformer(call_map).visit(procedure.body)

return inner
Loading
Loading