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

Draft: Proposal: resolution of the concrete procedure #340

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
39 changes: 38 additions & 1 deletion loki/expression/mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@


__all__ = ['LokiStringifyMapper', 'ExpressionRetriever', 'ExpressionDimensionsMapper',
'ExpressionCallbackMapper', 'SubstituteExpressionsMapper',
'ExpressionTypeMapper', 'ExpressionCallbackMapper', 'SubstituteExpressionsMapper',
'LokiIdentityMapper', 'AttachScopesMapper', 'DetachScopesMapper']


Expand Down Expand Up @@ -426,6 +426,43 @@ def map_inline_do(self, expr, *args, **kwargs):
return self.rec(expr.bounds, *args, **kwargs)


class ExpressionTypeMapper(Mapper):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very neat!

A small naming suggestion, since we implicitly make that distinction in the type system: I would call this ExpressionDatatypeMapper because "type" is what we use for SymbolAttributes, which include Datatype (dtype) as well as other declaration attributes.

"""
A visitor for an expression that determines the type of the expression.
This is a WIP implementation (missing, e.g.: handling of kinds, implicit type conversions)
"""
# pylint: disable=abstract-method,unused-argument

def map_float_literal(self, expr, *args, **kwargs):
return BasicType.REAL

def map_int_literal(self, expr, *args, **kwargs):
return BasicType.INTEGER

def map_logic_literal(self, expr, *args, **kwargs):
return BasicType.LOGICAL

def map_string_literal(self, expr, *args, **kwargs):
return BasicType.CHARACTER

def map_scalar(self, expr, *args, **kwargs):
return expr.type.dtype

map_array = map_scalar

def map_sum(self, expr, *args, **kwargs):
left = self.rec(expr.children[0], *args, **kwargs)
right = self.rec(expr.children[1], *args, **kwargs)
# INTEGER can be promoted to REAL
if left == BasicType.REAL and right == BasicType.INTEGER \
or left == BasicType.INTEGER and right == BasicType.REAL:
return BasicType.REAL
if left != right:
raise ValueError(f'Non-matching types: {str(left)} and {str(right)}')
return left

map_product = map_sum

class ExpressionCallbackMapper(CombineMapper):
"""
A visitor for expressions that returns the combined result of a specified callback function.
Expand Down
38 changes: 37 additions & 1 deletion loki/frontend/fparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from loki.expression.operations import (
StringConcat, ParenthesisedAdd, ParenthesisedMul, ParenthesisedDiv, ParenthesisedPow
)
from loki.expression import ExpressionDimensionsMapper, AttachScopes, AttachScopesMapper
from loki.expression import ExpressionDimensionsMapper, ExpressionTypeMapper, AttachScopes, AttachScopesMapper
from loki.logging import debug, perf, info, warning, error
from loki.tools import (
as_tuple, flatten, CaseInsensitiveDict, LazyNodeLookup, dict_override
Expand Down Expand Up @@ -2483,6 +2483,42 @@
arguments = tuple(arg for arg in arguments if not isinstance(arg, tuple))
else:
arguments, kwarguments = (), ()
# Figure out the exact procedure being called if this is a call to a generic interface
if name.type.dtype.is_generic:
# If the interface is imported, take its definition from a module
# TODO: handle interfaces defined in the same module
if name.type.imported:
module = name.type.module
interface = [i for i in module.interfaces if i.spec.name == name.name]
if len(interface) == 1:
interface = interface[0]
Comment on lines +2492 to +2494
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to shortcut this with

Suggested change
interface = [i for i in module.interfaces if i.spec.name == name.name]
if len(interface) == 1:
interface = interface[0]
interface = module.interface_map[name.name]

# Generic interface contains an abstract function definition and concrete function implementations.
# We need to get rid of the abstract definition.
concrete_symbols = [symbol for symbol in interface.symbols if not symbol.type.dtype.is_generic]

expr_type_mapper = ExpressionTypeMapper()
expr_dim_mapper = ExpressionDimensionsMapper()

passed_arguments_types = [(expr_type_mapper(arg), expr_dim_mapper(arg)) for arg in arguments]

# Try match passed arguments with one of the concrete functions from the interface
for symbol in concrete_symbols:
parameters = symbol.type.dtype.parameters
declared_parameters_types = [(expr_type_mapper(param), expr_dim_mapper(param)) for param in parameters]

Check failure on line 2507 in loki/frontend/fparser.py

View workflow job for this annotation

GitHub Actions / code checks (3.11)

C0301: Line too long (127/120) (line-too-long)
# Find matching concrete function
if declared_parameters_types == passed_arguments_types:
ptype: ProcedureType = name.type.dtype
new_procedure_type = ProcedureType(name=name.name,
is_function=ptype.is_function,
is_generic=ptype.is_generic,
return_type=ptype.return_type,
concrete_procedure=symbol.type.dtype.procedure)
new_symbol_attribute = SymbolAttributes(dtype=new_procedure_type,
imported=name.type.imported,
module=name.type.module)
# When the scope is name.scope, then the concrete_procedure takes always the same value (
# bug or my incomprehension?)
Comment on lines +2519 to +2520
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is precisely the problem that I'm describing. Hence my alternative suggestion which ensures that you can attach the correct scope to the symbol. We will enforce a strict requirement for having a symbol scope in the future, hence this workaround would no longer work.

name = sym.ProcedureSymbol(name=name.name, scope=None, type=new_symbol_attribute)
Comment on lines +2486 to +2521
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that attaching this information to the procedure type will work as expected. The reason is, that all symbols in a scope with the same name share the same entry in the symbol table. Therefore, your example of an int and real variant of the same generic function will create a conflict.

I think the above logic will have to be moved into the CallStatement node, either into the procedure_type or routine property:

loki/loki/ir/nodes.py

Lines 1039 to 1073 in b93a01d

def procedure_type(self):
"""
The :any:`ProcedureType` of the :any:`Subroutine` object of the called routine
For a :class:`CallStatement` node called ``call``, this is shorthand for ``call.name.type.dtype``.
If the procedure type object has been linked up with the corresponding
:any:`Subroutine` object, then it is available via ``call.procedure_type.procedure``.
Returns
-------
:any:`ProcedureType` or :any:`BasicType.DEFERRED`
The type of the called procedure. If the symbol type of the called routine
has not been identified correctly, this may yield :any:`BasicType.DEFERRED`.
"""
return self.name.type.dtype
@property
def routine(self):
"""
The :any:`Subroutine` object of the called routine
Shorthand for ``call.name.type.dtype.procedure``
Returns
-------
:any:`Subroutine` or :any:`BasicType.DEFERRED`
If the :any:`ProcedureType` object of the :any:`ProcedureSymbol`
in :attr:`name` is linked up to the target routine, this returns
the corresponding :any:`Subroutine` object, otherwise `None`.
"""
procedure_type = self.procedure_type
if procedure_type is BasicType.DEFERRED:
return BasicType.DEFERRED
return procedure_type.procedure

This has also other benefits:

  • The procedure chasing is performed only when required - making the frontend faster in all other situations.
  • The property is dynamic, which ensure this stays up-to-date when other IR transformations change the arguments. For example, we apply variable promotion/demotion, which might cause a different implementation of a generic function to become the correct procedure.
  • The feature is not tied to a single frontend but becomes available to others, too.

return ir.CallStatement(name=name, arguments=arguments, kwarguments=kwarguments,
label=kwargs.get('label'), source=kwargs.get('source'))

Expand Down
54 changes: 54 additions & 0 deletions loki/frontend/tests/test_frontends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2066,3 +2066,57 @@ def test_import_of_private_symbols(here, frontend):
assert var.type.imported is True
# Check if the symbol comes from the mod_public module
assert var.type.module is mod_public


@pytest.mark.parametrize('frontend', [FP])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please parameterize tests always with available_frontends(). If a test cannot be made to work with one of the frontends, that test realization should be explicitly skipped/xfailed instead.

def test_resolution_of_generic_procedures_ext_module(here, frontend):

code_swap_module = """
module swap_module
implicit none
interface swap
module procedure swap_int, swap_real
end interface swap
contains
subroutine swap_int(a, b)
integer, intent(inout) :: a, b
integer :: temp
temp = a
a = b
b = temp
end subroutine swap_int

subroutine swap_real(a, b)
real, intent(inout) :: a, b
real :: temp
temp = a
a = b
b = temp
end subroutine swap_real
end module swap_module
"""
code_main_module = """
module main
use swap_module, only: swap
contains
subroutine test()
real :: r1, r2
integer :: i1, i2
r1 = 0.0
r2 = 3.0
call swap(r1, r2)
i1 = 1
i2 = 3
call swap(i1, i2)
end subroutine
end module main
"""
mod_swap = Module.from_source(code_swap_module, frontend=frontend)
mod_main = Module.from_source(code_main_module, frontend=frontend, definitions=[mod_swap])
Comment on lines +2114 to +2115
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the tmp_path fixture to the test and use that as xmods directory here, to avoid that OMNI litters the current working directory with its xmod files.

Suggested change
mod_swap = Module.from_source(code_swap_module, frontend=frontend)
mod_main = Module.from_source(code_main_module, frontend=frontend, definitions=[mod_swap])
mod_swap = Module.from_source(code_swap_module, frontend=frontend, xmods=[tmp_path])
mod_main = Module.from_source(code_main_module, frontend=frontend, definitions=[mod_swap], xmods=[tmp_path])

# Procedures are defined in order: swap_int, swap_real
procedure_symbols = [routine for routine in mod_swap.subroutines]
test_routine = mod_main.subroutines[0]
calls = FindNodes(ir.CallStatement).visit(test_routine.body)

assert calls[0].procedure_type.concrete_procedure == procedure_symbols[1] # swap_real
assert calls[1].procedure_type.concrete_procedure == procedure_symbols[0] # swap_int
Comment on lines +2121 to +2122
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the above remark about moving the resolution to the CallStatement node, this should then be modified. Also, I think we might want to use the stronger is comparison here.

Suggested change
assert calls[0].procedure_type.concrete_procedure == procedure_symbols[1] # swap_real
assert calls[1].procedure_type.concrete_procedure == procedure_symbols[0] # swap_int
assert calls[0].routine is mod_swap['swap_real']
assert calls[1].routine is mod_swap['swap_int']

16 changes: 15 additions & 1 deletion loki/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,12 @@
Indicate that this is a generic function
procedure : :any:`Subroutine` or :any:`StatementFunction` or :any:`LazyNodeLookup`, optional
The procedure this type represents
concrete_procedure: :any:`Subroutine`, optional
The real procedure called when a generic functions is used
"""

def __init__(self, name=None, is_function=None, is_generic=False, procedure=None, return_type=None):
def __init__(self, name=None, is_function=None, is_generic=False, procedure=None, return_type=None,
concrete_procedure=None):
from loki.subroutine import Subroutine # pylint: disable=import-outside-toplevel,cyclic-import
super().__init__()
assert name or isinstance(procedure, Subroutine)
Expand All @@ -195,6 +198,10 @@
self._is_function = self.procedure.is_function
# TODO: compare return type once type comparison is more robust
self._return_type = self.procedure.return_type
if not self.is_generic:
self._concrete_procedure = self._procedure
else:
self._concrete_procedure = weakref.ref(concrete_procedure) if concrete_procedure is not None else None

@property
def _canonical(self):
Expand Down Expand Up @@ -232,6 +239,13 @@
return BasicType.DEFERRED
return self._procedure()

@property
def concrete_procedure(self):
if self._concrete_procedure is None:

Check failure on line 244 in loki/types.py

View workflow job for this annotation

GitHub Actions / code checks (3.11)

R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return)
return self.procedure
else:
return self._concrete_procedure()

@property
def parameters(self):
"""
Expand Down
Loading