Skip to content

Commit

Permalink
Merge pull request #416 from ecmwf-ifs/naml-intrinsic-procedure-type
Browse files Browse the repository at this point in the history
Expressions: Handle intrinsic function calls
  • Loading branch information
reuterbal authored Nov 21, 2024
2 parents ebc7b69 + bd243ac commit 72d35fb
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 14 deletions.
2 changes: 1 addition & 1 deletion loki/batch/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ def _dependencies(self):
inline_calls = tuple({
call.function.name: call.function
for call in FindInlineCalls().visit(self.ir.ir)
if isinstance(call.function, ProcedureSymbol)
if isinstance(call.function, ProcedureSymbol) and not call.function.type.is_intrinsic
}.values())
imports = tuple(
imprt for imprt in self.ir.imports
Expand Down
1 change: 1 addition & 0 deletions loki/expression/mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ def map_variable_symbol(self, expr, *args, **kwargs):
new_type = expr.type
if recurse_to_declaration_attributes:
old_type = expr.type
assert expr.type is not None
kind = self.rec(old_type.kind, *args, **kwargs)

if expr.scope and expr.name == old_type.initial:
Expand Down
41 changes: 40 additions & 1 deletion loki/expression/tests/test_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,53 @@ def test_math_intrinsics(tmp_path, frontend):
"""
filepath = tmp_path/(f'expression_math_intrinsics_{frontend}.f90')
routine = Subroutine.from_source(fcode, frontend=frontend)
function = jit_compile(routine, filepath=filepath, objname='math_intrinsics')

for assign in FindNodes(ir.Assignment).visit(routine.body):
assert isinstance(assign.rhs, sym.InlineCall)
assert isinstance(assign.rhs.function, sym.ProcedureSymbol)
assert assign.rhs.function.type.dtype.is_intrinsic

# Test full functionality via JIT example
function = jit_compile(routine, filepath=filepath, objname='math_intrinsics')
vmin, vmax, vabs, vexp, vsqrt, vlog = function(2., 4.)
assert vmin == 2. and vmax == 4. and vabs == 2.
assert vexp == np.exp(6.) and vsqrt == np.sqrt(6.) and vlog == np.log(6.)
clean_test(filepath)


@pytest.mark.parametrize('frontend', available_frontends())
def test_general_intrinsics(frontend):
"""
Test general intrinsic functions (size, shape, ubound, lbound,
allocated, trim, kind)
"""
fcode = """
subroutine general_intrinsics(arr, ptr, name)
implicit none
real(kind=8), intent(inout) :: arr(:,:)
real(kind=8), pointer, intent(inout) :: ptr(:,:)
character(len=*), intent(inout) :: name
integer :: isize, ishape(:), ilower, iupper, mykind
logical :: alloc
character(len=*) :: myname
isize = size(arr)
ishape = shape(arr)
ilower = lbound(arr)
iupper = ubound(arr)
mykind = kind(arr)
alloc = allocated(ptr)
myname = trim(name)
end subroutine general_intrinsics
"""
routine = Subroutine.from_source(fcode, frontend=frontend)

for assign in FindNodes(ir.Assignment).visit(routine.body):
assert isinstance(assign.rhs, sym.InlineCall)
assert isinstance(assign.rhs.function, sym.ProcedureSymbol)
assert assign.rhs.function.type.dtype.is_intrinsic


@pytest.mark.parametrize('frontend', available_frontends())
def test_logicals(tmp_path, frontend):
"""
Expand Down
13 changes: 13 additions & 0 deletions loki/frontend/fparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2547,7 +2547,20 @@ def visit_Function_Reference(self, o, **kwargs):


def visit_Intrinsic_Function_Reference(self, o, **kwargs):

# Register the ProcedureType in the scope before the name lookup
pname = o.children[0].string
scope = kwargs['scope']
if not scope.get_symbol_scope(pname):
# No known alternative definition; register a true intrinsic procedure type
proc_type = ProcedureType(
name=pname, is_function=True, is_intrinsic=True, procedure=None
)
kwargs['scope'].symbol_attrs[pname] = SymbolAttributes(dtype=proc_type, is_intrinsic=True)

# Look up the function symbol
name = self.visit(o.children[0], **kwargs)

if o.children[1] is not None:
arguments = self.visit(o.children[1], **kwargs)
kwarguments = tuple(arg for arg in arguments if isinstance(arg, tuple))
Expand Down
9 changes: 9 additions & 0 deletions loki/frontend/omni.py
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,15 @@ def visit_FarrayConstructor(self, o, **kwargs):
return sym.LiteralList(values=values, dtype=dtype)

def visit_functionCall(self, o, **kwargs):

if 'is_intrinsic' in o.attrib:
# Register the ProcedureType in the scope before the name lookup
pname = o.find('name').text
proc_type = ProcedureType(
name=pname, is_function=True, is_intrinsic=True, procedure=None
)
kwargs['scope'].symbol_attrs[pname] = SymbolAttributes(dtype=proc_type, is_intrinsic=True)

if o.find('name') is not None:
name = self.visit(o.find('name'), **kwargs)
elif o.find('FmemberRef') is not None:
Expand Down
77 changes: 77 additions & 0 deletions loki/frontend/tests/test_frontends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2127,3 +2127,80 @@ def test_import_of_private_symbols(tmp_path, 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', available_frontends(
xfail=[(OMNI, 'OMNI does not like intrinsic shading for member functions!')]
))
def test_intrinsic_shadowing(tmp_path, frontend):
"""
Test that locally defined functions that shadow intrinsics are handled.
"""
fcode_algebra = """
module algebra_mod
implicit none
contains
function dot_product(a, b) result(c)
real(kind=8), intent(inout) :: a(:), b(:)
real(kind=8) :: c
end function dot_product
function min(x, y)
real(kind=8), intent(in) :: x, y
real(kind=8) :: min
min = y
if (x < y) min = x
end function min
end module algebra_mod
"""

fcode = """
module test_intrinsics_mod
use algebra_mod, only: dot_product
implicit none
contains
subroutine test_intrinsics(a, b, c, d)
use algebra_mod, only: min
implicit none
real(kind=8), intent(inout) :: a(:), b(:)
real(kind=8) :: c, d, e
c = dot_product(a, b)
d = max(c, a(1))
e = min(c, a(1))
contains
function max(x, y)
real(kind=8), intent(in) :: x, y
real(kind=8) :: max
max = y
if (x > y) max = x
end function max
end subroutine test_intrinsics
end module test_intrinsics_mod
"""
algebra = Module.from_source(fcode_algebra, frontend=frontend, xmods=[tmp_path])
module = Module.from_source(
fcode, definitions=algebra, frontend=frontend, xmods=[tmp_path]
)
routine = module['test_intrinsics']

assigns = FindNodes(ir.Assignment).visit(routine.body)
assert len(assigns) == 3

assert isinstance(assigns[0].rhs.function, sym.ProcedureSymbol)
assert not assigns[0].rhs.function.type.is_intrinsic
assert assigns[0].rhs.function.type.dtype.procedure == algebra['dot_product']

assert isinstance(assigns[1].rhs.function, sym.ProcedureSymbol)
assert not assigns[1].rhs.function.type.is_intrinsic
assert assigns[1].rhs.function.type.dtype.procedure == routine.members[0]

assert isinstance(assigns[2].rhs.function, sym.ProcedureSymbol)
assert not assigns[2].rhs.function.type.is_intrinsic
assert assigns[2].rhs.function.type.dtype.procedure == algebra['min']
2 changes: 1 addition & 1 deletion loki/ir/tests/test_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def test_find_variables_associates(frontend):
routine = Subroutine.from_source(fcode, frontend=frontend)

variables = FindVariables(unique=False).visit(routine.body)
assert len(variables) == 29
assert len(variables) == 27 if frontend == OMNI else 28
assert len([v for v in variables if v.name == 'v']) == 1
assert len([v for v in variables if v.name == 'm']) == 2

Expand Down
12 changes: 6 additions & 6 deletions loki/tests/test_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,11 +1330,11 @@ def test_module_enrichment_within_file(frontend, tmp_path):
integer, parameter :: j = 16
contains
integer function SUM(v)
integer function plus_one(v)
implicit none
integer, intent(in) :: v
SUM = v + 1
end function SUM
plus_one = v + 1
end function plus_one
end module foo
module test
Expand All @@ -1348,7 +1348,7 @@ def test_module_enrichment_within_file(frontend, tmp_path):
real(kind=rk), intent(inout) :: res
integer(kind=ik) :: i
do i = 1, n
res = res + SUM(j)
res = res + plus_one(j)
end do
end subroutine calc
end module test
Expand All @@ -1358,10 +1358,10 @@ def test_module_enrichment_within_file(frontend, tmp_path):
routine = source['calc']
calls = list(FindInlineCalls().visit(routine.body))
assert len(calls) == 1
assert calls[0].function == 'sum'
assert calls[0].function == 'plus_one'
assert calls[0].function.type.imported
assert calls[0].function.type.module is source['foo']
assert calls[0].function.type.dtype.procedure is source['sum']
assert calls[0].function.type.dtype.procedure is source['plus_one']
if frontend != OMNI:
# OMNI inlines parameters
assert calls[0].arguments[0].type.dtype == BasicType.INTEGER
Expand Down
4 changes: 2 additions & 2 deletions loki/transformations/tests/test_raw_stack_allocator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from loki.backend import fgen
from loki.batch import Scheduler, SchedulerConfig
from loki.dimension import Dimension
from loki.expression import DeferredTypeSymbol, InlineCall, IntLiteral
from loki.expression import DeferredTypeSymbol, InlineCall, IntLiteral, ProcedureSymbol
from loki.frontend import available_frontends, OMNI
from loki.ir import FindNodes, CallStatement, Assignment, Pragma
from loki.sourcefile import Sourcefile
Expand Down Expand Up @@ -282,7 +282,7 @@ def test_raw_stack_allocator_temporaries(frontend, block_dim, horizontal, direct
real = BasicType.REAL
logical = BasicType.LOGICAL
jprb = DeferredTypeSymbol('JPRB')
srk = InlineCall(function = DeferredTypeSymbol(name = 'SELECTED_REAL_KIND'),
srk = InlineCall(function = ProcedureSymbol(name = 'SELECTED_REAL_KIND'),
parameters = (IntLiteral(13), IntLiteral(300)))

stack_dict = kernel1_item.trafo_data[transformation._key]['stack_dict']
Expand Down
10 changes: 8 additions & 2 deletions loki/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,22 @@ class ProcedureType(DataType):
Indicate that this is a function
is_generic : bool, optional
Indicate that this is a generic function
is_intrinsic : bool, optional
Indicate that this is an intrinsic function
procedure : :any:`Subroutine` or :any:`StatementFunction` or :any:`LazyNodeLookup`, optional
The procedure this type represents
"""

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,
is_intrinsic=False, procedure=None, return_type=None
):
from loki.subroutine import Subroutine # pylint: disable=import-outside-toplevel,cyclic-import
super().__init__()
assert name or isinstance(procedure, Subroutine)
assert isinstance(return_type, SymbolAttributes) or procedure or not is_function
assert isinstance(return_type, SymbolAttributes) or procedure or not is_function or is_intrinsic
self.is_generic = is_generic
self.is_intrinsic = is_intrinsic
if procedure is None or isinstance(procedure, LazyNodeLookup):
self._procedure = procedure
self._name = name
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies = [
"coloredlogs", # optional for loki-build utility
"junit_xml", # optional for JunitXML output in loki-lint
"codetiming", # essential for scheduler and sourcefile timings
"pydantic>=2.0", # type checking for IR nodes
"pydantic>=2.0,<2.10.0", # type checking for IR nodes
]

[project.optional-dependencies]
Expand Down

0 comments on commit 72d35fb

Please sign in to comment.