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

Expressions: Handle intrinsic function calls #416

Merged
merged 9 commits into from
Nov 21, 2024
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we somehow verify here that this is indeed the intrinsic function that is used, and not a locally defined function that happens to be called the same as an intrinsic function?

A concrete example is DOT_PRODUCT, where an implementation in IFS shadows the intrinsic Fortran function. In that case, the procedure symbol should likely not be classified as intrinsic.

It would probably be good to have a test that covers such shadowing.

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
Loading