diff --git a/loki/backend/cgen.py b/loki/backend/cgen.py index 77190e1a1..bcce4071d 100644 --- a/loki/backend/cgen.py +++ b/loki/backend/cgen.py @@ -10,6 +10,7 @@ PREC_UNARY, PREC_LOGICAL_OR, PREC_LOGICAL_AND, PREC_NONE, PREC_CALL ) +from loki.logging import warning from loki.tools import as_tuple from loki.ir import Import, Stringifier, FindNodes from loki.expression import ( @@ -140,6 +141,10 @@ def map_c_reference(self, expr, enclosing_prec, *args, **kwargs): def map_c_dereference(self, expr, enclosing_prec, *args, **kwargs): return self.format(' (*%s)', self.rec(expr.expression, PREC_NONE, *args, **kwargs)) + def map_inline_call(self, expr, enclosing_prec, *args, **kwargs): + if expr.function.name.lower() == 'present': + return self.format('true /*ATTENTION: present({%s})*/', expr.parameters[0].name) + return super().map_inline_call(expr, enclosing_prec, *args, **kwargs) class CCodegen(Stringifier): """ @@ -192,6 +197,13 @@ def _subroutine_argument_pass_by(self, a): return '*' if a.type.pointer: return '*' + if a.type.optional: + return '*' + return '' + + def _subroutine_optional_args(self, a): + if a.type.optional: + warning(f'Argument "{a}" is optional! No support for optional arguments in {self.__class__.__name__}.') return '' def _subroutine_declaration(self, o, **kwargs): @@ -203,7 +215,7 @@ def _subroutine_declaration(self, o, **kwargs): # for a, p, k in zip(o.arguments, pass_by, var_keywords)] arguments = [ (f'{self._subroutine_argument_keyword(a)}{self.visit(a.type, **kwargs)} ' - f'{self._subroutine_argument_pass_by(a)}{a.name}') + f'{self._subroutine_argument_pass_by(a)}{a.name}{self._subroutine_optional_args(a)}') for a in o.arguments ] opt_header = kwargs.get('header', False) diff --git a/loki/backend/cppgen.py b/loki/backend/cppgen.py index b426a7372..2fd50ffe5 100644 --- a/loki/backend/cppgen.py +++ b/loki/backend/cppgen.py @@ -32,8 +32,12 @@ class CppCodeMapper(CCodeMapper): A :class:`StringifyMapper`-derived visitor for Pymbolic expression trees that converts an expression to a string adhering to standardized C++. """ - # pylint: disable=abstract-method, unused-argument, unnecessary-pass - pass + # pylint: disable=abstract-method, unused-argument + + def map_inline_call(self, expr, enclosing_prec, *args, **kwargs): + if expr.function.name.lower() == 'present': + return self.format('%s', expr.parameters[0].name) + return super().map_inline_call(expr, enclosing_prec, *args, **kwargs) class CppCodegen(CCodegen): @@ -68,6 +72,10 @@ def _subroutine_footer(self, o, **kwargs): footer += [self.format_line('\n} // extern')] if opt_extern else [] return footer + def _subroutine_optional_args(self, a): + if a.type.optional: + return ' = nullptr' + return '' def cppgen(ir, **kwargs): """ diff --git a/loki/build/compiler.py b/loki/build/compiler.py index 821029874..a6b1bbb7e 100644 --- a/loki/build/compiler.py +++ b/loki/build/compiler.py @@ -124,6 +124,8 @@ class Compiler: CC = None CFLAGS = None + CPP = None + CPPFLAGS = None F90 = None F90FLAGS = None FC = None @@ -137,6 +139,8 @@ class Compiler: def __init__(self): self.cc = self.CC or 'gcc' self.cflags = self.CFLAGS or ['-g', '-fPIC'] + self.cpp = self.CPP or 'g++' + self.cppflags = self.CPPFLAGS or ['-g', '-fPIC'] self.f90 = self.F90 or 'gfortran' self.f90flags = self.F90FLAGS or ['-g', '-fPIC'] self.fc = self.FC or 'gfortran' @@ -164,13 +168,13 @@ def compile_args(self, source, target=None, include_dirs=None, mod_dir=None, mod mode : str, optional One of ``'f90'`` (free form), ``'f'`` (fixed form) or ``'c'`` """ - assert mode in ['f90', 'f', 'c'] + assert mode in ['f90', 'f', 'c', 'cpp'] include_dirs = include_dirs or [] - cc = {'f90': self.f90, 'f': self.fc, 'c': self.cc}[mode] + cc = {'f90': self.f90, 'f': self.fc, 'c': self.cc, 'cpp': self.cpp}[mode] args = [cc, '-c'] - args += {'f90': self.f90flags, 'f': self.fcflags, 'c': self.cflags}[mode] + args += {'f90': self.f90flags, 'f': self.fcflags, 'c': self.cflags, 'cpp': self.cppflags}[mode] args += self._include_dir_args(include_dirs) - if mode != 'c': + if mode not in ['c', 'cpp']: args += self._mod_dir_args(mod_dir) args += [] if target is None else ['-o', str(target)] args += [str(source)] @@ -194,13 +198,15 @@ def _mod_dir_args(self, mod_dir): return [] return [f'-J{mod_dir!s}'] - def compile(self, source, target=None, include_dirs=None, use_c=False, cwd=None): + def compile(self, source, target=None, include_dirs=None, use_c=False, use_cpp=False, cwd=None): """ Execute a build command for a given source. """ kwargs = {'target': target, 'include_dirs': include_dirs} if use_c: kwargs['mode'] = 'c' + if use_cpp: + kwargs['mode'] = 'cpp' args = self.compile_args(source, **kwargs) execute(args, cwd=cwd) @@ -282,6 +288,8 @@ class GNUCompiler(Compiler): CC = 'gcc' CFLAGS = ['-g', '-fPIC'] + CPP = 'g++' + CPPFLAGS = ['-g', '-fPIC'] F90 = 'gfortran' F90FLAGS = ['-g', '-fPIC'] FC = 'gfortran' @@ -293,6 +301,7 @@ class GNUCompiler(Compiler): F2PY_FCOMPILER_TYPE = 'gnu95' CC_PATTERN = re.compile(r'(^|/|\\)gcc\b') + CPP_PATTERN = re.compile(r'(^|/|\\)g\+\+\b') FC_PATTERN = re.compile(r'(^|/|\\)gfortran\b') @@ -303,6 +312,8 @@ class NvidiaCompiler(Compiler): CC = 'nvc' CFLAGS = ['-g', '-fPIC'] + CPP = 'nvc++' + CPPFLAGS = ['-g', '-fPIC'] F90 = 'nvfortran' F90FLAGS = ['-g', '-fPIC'] FC = 'nvfortran' @@ -314,6 +325,7 @@ class NvidiaCompiler(Compiler): F2PY_FCOMPILER_TYPE = 'nv' CC_PATTERN = re.compile(r'(^|/|\\)nvc\b') + CPP_PATTERN = re.compile(r'(^|/|\\)nvc\+\+\b') FC_PATTERN = re.compile(r'(^|/|\\)(pgf9[05]|pgfortran|nvfortran)\b') def _mod_dir_args(self, mod_dir): @@ -358,7 +370,8 @@ def get_compiler_from_env(env=None): var_pattern_map = { 'F90': 'FC_PATTERN', 'FC': 'FC_PATTERN', - 'CC': 'CC_PATTERN' + 'CC': 'CC_PATTERN', + 'CPP': 'CPP_PATTERN' } for var, pattern in var_pattern_map.items(): if env.get(var): @@ -377,6 +390,7 @@ def get_compiler_from_env(env=None): # overwrite compiler executable and compiler flags with environment values var_compiler_map = { 'CC': 'cc', + 'CPP': 'cpp', 'FC': 'fc', 'F90': 'f90', 'LD': 'ld', @@ -388,6 +402,7 @@ def get_compiler_from_env(env=None): var_flag_map = { 'CFLAGS': 'cflags', + 'CPPFLAGS': 'cppflags', 'FCFLAGS': 'fcflags', 'LDFLAGS': 'ldflags', } diff --git a/loki/build/obj.py b/loki/build/obj.py index 054e21872..5d99fd204 100644 --- a/loki/build/obj.py +++ b/loki/build/obj.py @@ -30,11 +30,12 @@ class Obj: A single source object representing a single C or Fortran source file. """ - MODEMAP = {'.f90': 'f90', '.f': 'f', '.c': 'c', '.cc': 'c'} + MODEMAP = {'.f90': 'f90', '.f': 'f', '.c': 'c', '.cc': 'c', '.cpp': 'cpp', + '.CC': 'cpp', '.cxx': 'cpp'} # Default source and header extension recognized # TODO: Make configurable! - _ext = ['.f90', '.F90', '.f', '.F', '.c'] + _ext = ['.f90', '.F90', '.f', '.F', '.c', '.cpp', '.CC', '.cc', '.cxx'] def __new__(cls, *args, name=None, **kwargs): # pylint: disable=unused-argument # Name is either provided or inferred from source_path diff --git a/loki/transformations/transpile/fortran_c.py b/loki/transformations/transpile/fortran_c.py index c026819a1..5e487d086 100644 --- a/loki/transformations/transpile/fortran_c.py +++ b/loki/transformations/transpile/fortran_c.py @@ -8,7 +8,7 @@ from pathlib import Path from collections import OrderedDict -from loki.backend import cgen, fgen, cudagen +from loki.backend import cgen, fgen, cudagen, cppgen from loki.batch import Transformation from loki.expression import ( symbols as sym, Variable, InlineCall, RangeIndex, Scalar, Array, @@ -121,20 +121,26 @@ def __init__(self, inline_elementals=True, use_c_ptr=False, path=None, language= self.use_c_ptr = use_c_ptr self.path = Path(path) if path is not None else None self.language = language.lower() - assert self.language in ['c', 'cuda'] # , 'hip'] + self._supported_languages = ['c', 'cpp', 'cuda'] if self.language == 'c': self.codegen = cgen + elif self.language == 'cpp': + self.codegen = cppgen elif self.language == 'cuda': self.codegen = cudagen - # elif self.language == 'hip': - # self.langgen = hipgen else: - assert False + raise ValueError(f'language "{self.language}" is not supported!' + f' (supported languages: "{self._supported_languages}")') # Maps from original type name to ISO-C and C-struct types self.c_structs = OrderedDict() + def file_suffix(self): + if self.language == 'cpp': + return '.cpp' + return '.c' + def transform_module(self, module, **kwargs): if self.path is None: path = Path(kwargs.get('path')) @@ -188,7 +194,7 @@ def transform_subroutine(self, routine, **kwargs): if role == 'kernel': # Generate Fortran wrapper module - bind_name = None if self.language == 'c' else f'{routine.name.lower()}_c_launch' + bind_name = None if self.language in ['c', 'cpp'] else f'{routine.name.lower()}_c_launch' wrapper = self.generate_iso_c_wrapper_routine(routine, self.c_structs, bind_name=bind_name) contains = Section(body=(Intrinsic('CONTAINS'), wrapper)) self.wrapperpath = (path/wrapper.name.lower()).with_suffix('.F90') @@ -197,7 +203,7 @@ def transform_subroutine(self, routine, **kwargs): # Generate C source file from Loki IR c_kernel = self.generate_c_kernel(routine, targets=targets) - self.c_path = (path/c_kernel.name.lower()).with_suffix('.c') + self.c_path = (path/c_kernel.name.lower()).with_suffix(self.file_suffix()) Sourcefile.to_file(source=fgen(module), path=self.wrapperpath) # Generate C source file from Loki IR @@ -219,8 +225,9 @@ def transform_subroutine(self, routine, **kwargs): if depth > 1: c_kernel.spec.prepend(Import(module=f'{c_kernel.name.lower()}.h', c_import=True)) - self.c_path = (path/c_kernel.name.lower()).with_suffix('.c') - Sourcefile.to_file(source=self.codegen(c_kernel), path=self.c_path) + self.c_path = (path/c_kernel.name.lower()).with_suffix(self.file_suffix()) + Sourcefile.to_file(source=self.codegen(c_kernel, extern=self.language=='cpp'), + path=self.c_path) header_path = (path/c_kernel.name.lower()).with_suffix('.h') Sourcefile.to_file(source=self.codegen(c_kernel, header=True), path=header_path) @@ -442,7 +449,7 @@ def generate_iso_c_interface(self, routine, bind_name, c_structs, scope): else: # Only scalar, intent(in) arguments are pass by value # Pass by reference for array types - value = isinstance(arg, Scalar) and arg.type.intent.lower() == 'in' + value = isinstance(arg, Scalar) and arg.type.intent.lower() == 'in' and not arg.type.optional kind = self.iso_c_intrinsic_kind(arg.type, intf_routine, is_array=isinstance(arg, Array)) if self.use_c_ptr: if isinstance(arg, Array): @@ -525,7 +532,7 @@ def apply_de_reference(routine): """ to_be_dereferenced = [] for arg in routine.arguments: - if not(arg.type.intent.lower() == 'in' and isinstance(arg, Scalar)): + if not(arg.type.intent.lower() == 'in' and isinstance(arg, Scalar)) or arg.type.optional: to_be_dereferenced.append(arg.name.lower()) routine.body = DeReferenceTrafo(to_be_dereferenced).visit(routine.body) diff --git a/loki/transformations/transpile/tests/test_transpile.py b/loki/transformations/transpile/tests/test_transpile.py index 958adafcb..1f47da3f3 100644 --- a/loki/transformations/transpile/tests/test_transpile.py +++ b/loki/transformations/transpile/tests/test_transpile.py @@ -19,12 +19,23 @@ from loki.transformations.transpile import FortranCTransformation from loki.transformations.single_column import SCCLowLevelHoist, SCCLowLevelParametrise +# pylint: disable=too-many-lines + @pytest.fixture(scope='function', name='builder') def fixture_builder(tmp_path): yield Builder(source_dirs=tmp_path, build_dir=tmp_path) Obj.clear_cache() +def test_transpile_unsupported_lang(): + """ + A simple test for testing failure/exception for unsupported + language(s). + """ + with pytest.raises(ValueError): + FortranCTransformation(language='not-supported') + + @pytest.mark.parametrize('case_sensitive', (False, True)) @pytest.mark.parametrize('frontend', available_frontends()) @pytest.mark.parametrize('language', ('c', 'cuda')) @@ -1447,3 +1458,108 @@ def test_scc_cuda_hoist(tmp_path, here, frontend, config, horizontal, vertical, assert '#include' in c_elemental_device assert '#include' in c_elemental_device assert '#include"elemental_device_c.h"' in c_elemental_device + + +@pytest.mark.parametrize('frontend', available_frontends()) +@pytest.mark.parametrize('language', ['c', 'cpp']) +def test_transpile_optional_args(tmp_path, builder, frontend, language): + """ + A simple test to verify multiconditionals/select case statements. + """ + + fcode = """ +subroutine transpile_optional_args(in, out, out2, opt_flag) + implicit none + integer, intent(in) :: in + integer, intent(inout) :: out + integer, intent(out), optional :: out2 + logical, intent(in), optional :: opt_flag + + out = in + if (present(out2)) then + out2 = 2*in + if (present(opt_flag)) then + if (opt_flag) then + out = 2* out2 + else + out = 4* out2 + endif + else + out = out2 + endif + endif + if (.not. present(out2) .and. present(opt_flag)) then + if (opt_flag) then + out = in + 1 + else + out = in + 2 + endif + endif + +end subroutine transpile_optional_args +""".strip() + + def init_out_vars(): + return np.int_([0]), np.int_([0]) + + builder.clean() + # for testing purposes + in_var = 10 + + # compile and test original Fortran version + routine = Subroutine.from_source(fcode, frontend=frontend) + filepath = tmp_path/f'{routine.name}_{frontend!s}.f90' + function = jit_compile(routine, filepath=filepath, objname=routine.name) + out_var, out_var2 = init_out_vars() + function(in_var, out_var) + assert out_var == 10 and out_var2 == 0 + out_var, out_var2 = init_out_vars() + function(in_var, out_var, out_var2) + assert out_var == 20 and out_var2 == 20 + opt_flag = 1 + out_var, out_var2 = init_out_vars() + function(in_var, out_var, opt_flag=opt_flag) + assert out_var == 11 and out_var2 == 0 + opt_flag = 0 + out_var, out_var2 = init_out_vars() + function(in_var, out_var, opt_flag=opt_flag) + assert out_var == 12 and out_var2 == 0 + opt_flag = 1 + out_var, out_var2 = init_out_vars() + function(in_var, out_var, out_var2, opt_flag) + assert out_var == 40 and out_var2 == 20 + opt_flag = 0 + out_var, out_var2 = init_out_vars() + function(in_var, out_var, out_var2, opt_flag) + assert out_var == 80 and out_var2 == 20 + + clean_test(filepath) + + # transpile + f2c = FortranCTransformation(language=language) + f2c.apply(source=routine, path=tmp_path) + + # compile and testC/C++ version + libname = f'fc_{routine.name}_{language}_{frontend}' + c_kernel = jit_compile_lib([f2c.wrapperpath, f2c.c_path], path=tmp_path, name=libname, builder=builder) + fc_function = c_kernel.transpile_optional_args_fc_mod.transpile_optional_args_fc + if language != 'c': + out_var, out_var2 = init_out_vars() + fc_function(in_var, out_var) + assert out_var == 10 and out_var2 == 0 + opt_flag = 1 + out_var, out_var2 = init_out_vars() + fc_function(in_var, out_var, opt_flag=opt_flag) + assert out_var == 11 and out_var2 == 0 + opt_flag = 0 + out_var, out_var2 = init_out_vars() + fc_function(in_var, out_var, opt_flag=opt_flag) + assert out_var == 12 and out_var2 == 0 + opt_flag = 1 + out_var, out_var2 = init_out_vars() + fc_function(in_var, out_var, out_var2, opt_flag) + assert out_var == 40 and out_var2 == 20 + opt_flag = 0 + out_var, out_var2 = init_out_vars() + fc_function(in_var, out_var, out_var2, opt_flag) + assert out_var == 80 and out_var2 == 20