From 2d08a779cafbea6fdfc6df323315acc7ec43a7ff Mon Sep 17 00:00:00 2001 From: Michael Staneker Date: Tue, 9 Apr 2024 09:37:01 +0000 Subject: [PATCH] [F2C] started refactoring 'cgen' and introducing c-like codegen ('cppgen', 'cudagen') --- loki/backend/__init__.py | 2 + loki/backend/cgen.py | 136 +++++++++++++++++++------- loki/backend/cppgen.py | 102 +++++++++++++++++++ loki/backend/cudagen.py | 70 +++++++++++++ loki/tools/strings.py | 2 +- loki/transform/fortran_c_transform.py | 9 +- tests/test_transpile.py | 7 +- 7 files changed, 284 insertions(+), 44 deletions(-) create mode 100644 loki/backend/cppgen.py create mode 100644 loki/backend/cudagen.py diff --git a/loki/backend/__init__.py b/loki/backend/__init__.py index acacaa1d5..f6d03ecd3 100644 --- a/loki/backend/__init__.py +++ b/loki/backend/__init__.py @@ -7,6 +7,8 @@ from loki.backend.fgen import * # noqa from loki.backend.cgen import * # noqa +from loki.backend.cppgen import * # noqa +from loki.backend.cudagen import * # noqa from loki.backend.maxgen import * # noqa from loki.backend.pygen import * # noqa from loki.backend.dacegen import * # noqa diff --git a/loki/backend/cgen.py b/loki/backend/cgen.py index df590f619..fa7f1796f 100644 --- a/loki/backend/cgen.py +++ b/loki/backend/cgen.py @@ -17,37 +17,52 @@ symbols as sym ) from loki.types import BasicType, SymbolAttributes, DerivedType -__all__ = ['cgen', 'CCodegen', 'CCodeMapper'] +__all__ = ['cgen', 'CCodegen', 'CCodeMapper', 'IntrinsicTypeC'] -def c_intrinsic_type(_type): - if _type.dtype == BasicType.LOGICAL: - return 'int' - if _type.dtype == BasicType.INTEGER: - return 'int' - if _type.dtype == BasicType.REAL: - if str(_type.kind) in ['real32']: - return 'float' - return 'double' - raise ValueError(str(_type)) +class IntrinsicTypeC: + # pylint: disable=abstract-method, unused-argument + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __call__(self, _type, *args, **kwargs): + return self.c_intrinsic_type(_type, *args, **kwargs) + def c_intrinsic_type(self, _type, *args, **kwargs): + if _type.dtype == BasicType.LOGICAL: + return 'int' + if _type.dtype == BasicType.INTEGER: + return 'int' + if _type.dtype == BasicType.REAL: + if str(_type.kind) in ['real32']: + return 'float' + return 'double' + raise ValueError(str(_type)) + +# pylint: disable=redefined-outer-name +c_intrinsic_type = IntrinsicTypeC() class CCodeMapper(LokiStringifyMapper): # pylint: disable=abstract-method, unused-argument + def __init__(self, c_intrinsic_type, *args, **kwargs): + super().__init__() + self.c_intrinsic_type = c_intrinsic_type + def map_logic_literal(self, expr, enclosing_prec, *args, **kwargs): return super().map_logic_literal(expr, enclosing_prec, *args, **kwargs).lower() def map_float_literal(self, expr, enclosing_prec, *args, **kwargs): if expr.kind is not None: _type = SymbolAttributes(BasicType.REAL, kind=expr.kind) - return f'({c_intrinsic_type(_type)}) {str(expr.value)}' + return f'({self.c_intrinsic_type(_type)}) {str(expr.value)}' return str(expr.value) def map_int_literal(self, expr, enclosing_prec, *args, **kwargs): if expr.kind is not None: _type = SymbolAttributes(BasicType.INTEGER, kind=expr.kind) - return f'({c_intrinsic_type(_type)}) {str(expr.value)}' + return f'({self.c_intrinsic_type(_type)}) {str(expr.value)}' return str(expr.value) def map_string_literal(self, expr, enclosing_prec, *args, **kwargs): @@ -59,7 +74,7 @@ def map_cast(self, expr, enclosing_prec, *args, **kwargs): self.join_rec('', expr.parameters, PREC_NONE, *args, **kwargs), PREC_CALL, PREC_NONE) return self.parenthesize_if_needed( - self.format('(%s) %s', c_intrinsic_type(_type), expression), enclosing_prec, PREC_CALL) + self.format('(%s) %s', self.c_intrinsic_type(_type), expression), enclosing_prec, PREC_CALL) def map_variable_symbol(self, expr, enclosing_prec, *args, **kwargs): if expr.parent is not None: @@ -122,10 +137,15 @@ class CCodegen(Stringifier): """ Tree visitor to generate standardized C code from IR. """ + # pylint: disable=abstract-method, unused-argument - def __init__(self, depth=0, indent=' ', linewidth=90): + standard_imports = ['stdio.h', 'stdbool.h', 'float.h', 'math.h'] + + def __init__(self, depth=0, indent=' ', linewidth=90, **kwargs): + symgen = kwargs.get('symgen', CCodeMapper(c_intrinsic_type)) + line_cont = kwargs.get('line_cont', '\n{} '.format) super().__init__(depth=depth, indent=indent, linewidth=linewidth, - line_cont='\n{} '.format, symgen=CCodeMapper()) + line_cont=line_cont, symgen=symgen) # Handler for outer objects @@ -143,25 +163,32 @@ def visit_Module(self, o, **kwargs): routines = self.visit(o.routines, **kwargs) return self.join_lines(spec, routines) - def visit_Subroutine(self, o, **kwargs): - """ - Format as: - - ...imports... - int () { - ...spec without imports and argument declarations... - ...body... - } - """ + def _subroutine_header(self, o, **kwargs): # Some boilerplate imports... - standard_imports = ['stdio.h', 'stdbool.h', 'float.h', 'math.h'] - header = [self.format_line('#include <', name, '>') for name in standard_imports] - + header = [self.format_line('#include <', name, '>') for name in self.standard_imports] # ...and imports from the spec spec_imports = FindNodes(Import).visit(o.spec) header += [self.visit(spec_imports, **kwargs)] + return header + def _subroutine_arguments(self, o, **kwargs): + var_keywords = [] + pass_by = [] + for a in o.arguments: + var_keywords += [''] + if isinstance(a, Array) > 0: + pass_by += ['* restrict '] + elif isinstance(a.type.dtype, DerivedType): + pass_by += ['*'] + elif a.type.pointer: + pass_by += ['*'] + else: + pass_by += [''] + return pass_by, var_keywords + + def _subroutine_declaration(self, o, **kwargs): # Generate header with argument signature + """ aptr = [] for a in o.arguments: if isinstance(a, Array) > 0: @@ -172,12 +199,19 @@ def visit_Subroutine(self, o, **kwargs): aptr += ['*'] else: aptr += [''] - arguments = [f'{self.visit(a.type, **kwargs)} {p}{a.name.lower()}' - for a, p in zip(o.arguments, aptr)] - header += [self.format_line('int ', o.name, '(', self.join_items(arguments), ') {')] - + """ + pass_by, var_keywords = self._subroutine_arguments(o, **kwargs) + arguments = [f'{k}{self.visit(a.type, **kwargs)} {p}{a.name.lower()}' + for a, p, k in zip(o.arguments, pass_by, var_keywords)] + opt_header = kwargs.get('header', False) + end = ' {' if not opt_header else ';' + declaration = [self.format_line('int ', o.name, '(', self.join_items(arguments), ')', end)] + return declaration + + def _subroutine_body(self, o, **kwargs): self.depth += 1 + # body = ['{'] # ...and generate the spec without imports and argument declarations body = [self.visit(o.spec, skip_imports=True, skip_argument_declarations=True, **kwargs)] @@ -187,9 +221,37 @@ def visit_Subroutine(self, o, **kwargs): # Close everything off self.depth -= 1 + # footer = [self.format_line('}')] + return body + + def _subroutine_footer(self, o, **kwargs): footer = [self.format_line('}')] + return footer + + def visit_Subroutine(self, o, **kwargs): + """ + Format as: + + ...imports... + int () { + ...spec without imports and argument declarations... + ...body... + } + """ + opt_header = kwargs.get('header', False) + opt_guards = kwargs.get('guards', False) + + header = self._subroutine_header(o, **kwargs) + declaration = self._subroutine_declaration(o, **kwargs) + body = self._subroutine_body(o, **kwargs) if not opt_header else [] + footer = self._subroutine_footer(o, **kwargs) if not opt_header else [] + + if opt_guards: + guard_name = f'{o.name.upper()}_H' + header = [self.format_line(f'#ifndef {guard_name}'), self.format_line(f'#define {guard_name}\n\n')] + header + footer += ['\n#endif'] - return self.join_lines(*header, *body, *footer) + return self.join_lines(*header, '\n', *declaration, *body, *footer) # Handler for AST base nodes @@ -357,7 +419,7 @@ def visit_CallStatement(self, o, **kwargs): def visit_SymbolAttributes(self, o, **kwargs): # pylint: disable=unused-argument if isinstance(o.dtype, DerivedType): return f'struct {o.dtype.name}' - return c_intrinsic_type(o) + return self.symgen.c_intrinsic_type(o) def visit_TypeDef(self, o, **kwargs): header = self.format_line('struct ', o.name.lower(), ' {') @@ -402,8 +464,8 @@ def visit_MultiConditional(self, o, **kwargs): return self.join_lines(header, *branches, footer) -def cgen(ir): +def cgen(ir, **kwargs): """ Generate standardized C code from one or many IR objects/trees. """ - return CCodegen().visit(ir) + return CCodegen().visit(ir, **kwargs) diff --git a/loki/backend/cppgen.py b/loki/backend/cppgen.py new file mode 100644 index 000000000..d6a17e95f --- /dev/null +++ b/loki/backend/cppgen.py @@ -0,0 +1,102 @@ +# (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.expression import Array +from loki.types import BasicType, DerivedType +from loki.backend.cgen import CCodegen, CCodeMapper, IntrinsicTypeC + +__all__ = ['cppgen', 'CppCodegen', 'CppCodeMapper', 'IntrinsicTypeCpp'] + + +class IntrinsicTypeCpp(IntrinsicTypeC): + + def c_intrinsic_type(self, _type, *args, **kwargs): + if _type.dtype == BasicType.INTEGER: + if _type.parameter: + return 'const int' + return 'int' + return super().c_intrinsic_type(_type, *args, **kwargs) + +cpp_intrinsic_type = IntrinsicTypeCpp() + + +class CppCodeMapper(CCodeMapper): + # pylint: disable=abstract-method, unused-argument + pass + + +class CppCodegen(CCodegen): + """ + ... + """ + standard_imports = ['stdio.h', 'stdbool.h', 'float.h', 'math.h'] + + def __init__(self, depth=0, indent=' ', linewidth=90, **kwargs): + symgen = kwargs.get('symgen', CppCodeMapper(cpp_intrinsic_type)) + line_cont = kwargs.get('line_cont', '\n{} '.format) + + super().__init__(depth=depth, indent=indent, linewidth=linewidth, + line_cont=line_cont, symgen=symgen) + + def _subroutine_header(self, o, **kwargs): + header = super()._subroutine_header(o, **kwargs) + return header + + def _subroutine_arguments(self, o, **kwargs): + # opt_extern = kwargs.get('extern', False) + # if opt_extern: + # return super()._subroutine_arguments(o, **kwargs) + var_keywords = [] + pass_by = [] + for a in o.arguments: + if isinstance(a, Array) > 0 and a.type.intent.lower() == "in": + var_keywords += ['const '] + else: + var_keywords += [''] + if isinstance(a, Array) > 0: + pass_by += ['* restrict '] + elif isinstance(a.type.dtype, DerivedType): + pass_by += ['*'] + elif a.type.pointer: + pass_by += ['*'] + else: + pass_by += [''] + return pass_by, var_keywords + + def _subroutine_declaration(self, o, **kwargs): + opt_extern = kwargs.get('extern', False) + declaration = [self.format_line('extern "C" {\n')] if opt_extern else [] + declaration += super()._subroutine_declaration(o, **kwargs) + return declaration + + def _subroutine_body(self, o, **kwargs): + body = super()._subroutine_body(o, **kwargs) + return body + + def _subroutine_footer(self, o, **kwargs): + opt_extern = kwargs.get('extern', False) + footer = super()._subroutine_footer(o, **kwargs) + footer += [self.format_line('\n} // extern')] if opt_extern else [] + return footer + + # def visit_Subroutine(self, o, **kwargs): + # """ + # Format as: + # ...imports... + # int () { + # ...spec without imports and argument declarations... + # ...body... + # } + # """ + # return super().visit_Subroutine(o, **kwargs) + + +def cppgen(ir, **kwargs): + """ + ... + """ + return CppCodegen().visit(ir, **kwargs) diff --git a/loki/backend/cudagen.py b/loki/backend/cudagen.py new file mode 100644 index 000000000..1779dec6a --- /dev/null +++ b/loki/backend/cudagen.py @@ -0,0 +1,70 @@ +# (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. +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. + +from loki.expression import Array +from loki.types import DerivedType +from loki.backend.cppgen import CppCodegen, CppCodeMapper, IntrinsicTypeCpp + +__all__ = ['cudagen', 'CudaCodegen', 'CudaCodeMapper'] + + +class IntrinsicTypeCuda(IntrinsicTypeCpp): + pass + +cuda_intrinsic_type = IntrinsicTypeCuda() + + +class CudaCodeMapper(CppCodeMapper): + # pylint: disable=abstract-method, unused-argument + pass + + +class CudaCodegen(CppCodegen): + """ + ... + """ + + standard_imports = ['stdio.h', 'stdbool.h', 'float.h', + 'math.h', 'cuda.h', 'cuda_runtime.h'] + + def __init__(self, depth=0, indent=' ', linewidth=90, **kwargs): + symgen = kwargs.get('symgen', CudaCodeMapper(cuda_intrinsic_type)) + line_cont = kwargs.get('line_cont', '\n{} '.format) + + super().__init__(depth=depth, indent=indent, linewidth=linewidth, + line_cont=line_cont, symgen=symgen) + + + def _subroutine_arguments(self, o, **kwargs): + var_keywords = [] + pass_by = [] + for a in o.arguments: + if isinstance(a, Array) > 0 and a.type.intent.lower() == "in": + var_keywords += ['const '] + else: + var_keywords += [''] + if isinstance(a, Array) > 0: + pass_by += ['* __restrict__ '] + elif isinstance(a.type.dtype, DerivedType): + pass_by += ['*'] + elif a.type.pointer: + pass_by += ['*'] + else: + pass_by += [''] + return pass_by, var_keywords + + def visit_CallStatement(self, o, **kwargs): + args = self.visit_all(o.arguments, **kwargs) + assert not o.kwarguments + chevron = f'<<<{",".join([str(elem) for elem in o.chevron])}>>>' if o.chevron is not None else '' + return self.format_line(str(o.name).lower(), chevron, '(', self.join_items(args), ');') + + +def cudagen(ir, **kwargs): + """ + ... + """ + return CudaCodegen().visit(ir, **kwargs) diff --git a/loki/tools/strings.py b/loki/tools/strings.py index dd3377770..5d31466df 100644 --- a/loki/tools/strings.py +++ b/loki/tools/strings.py @@ -167,7 +167,7 @@ def _to_str(self, line='', stop_on_continuation=False): continue sep = self.sep if idx + 1 < len(self.items) else '' old_line = line - line, _lines = self._add_item_to_line(line, item + sep) + line, _lines = self._add_item_to_line(line, str(item) + sep) if stop_on_continuation and _lines: return old_line, type(self)(self.items[idx:], sep=self.sep, width=self.width, cont=self.cont, separable=self.separable) diff --git a/loki/transform/fortran_c_transform.py b/loki/transform/fortran_c_transform.py index dd0aa2e00..e6af00065 100644 --- a/loki/transform/fortran_c_transform.py +++ b/loki/transform/fortran_c_transform.py @@ -52,6 +52,8 @@ class FortranCTransformation(Transformation): use_c_ptr : bool, optional Use ``c_ptr`` for array declarations in the F2C wrapper and ``c_loc(...)`` to pass the corresponding argument. Default is ``False``. + codegen : + Wrapper function calling the Stringifier instance. path : str, optional Path to generate C sources. """ @@ -60,10 +62,11 @@ class FortranCTransformation(Transformation): # Set of standard module names that have no C equivalent __fortran_intrinsic_modules = ['ISO_FORTRAN_ENV', 'ISO_C_BINDING'] - def __init__(self, inline_elementals=True, use_c_ptr=False, path=None): + def __init__(self, inline_elementals=True, use_c_ptr=False, path=None, codegen=cgen): self.inline_elementals = inline_elementals self.use_c_ptr = use_c_ptr self.path = Path(path) if path is not None else None + self.codegen = codegen # Maps from original type name to ISO-C and C-struct types self.c_structs = OrderedDict() @@ -87,7 +90,7 @@ def transform_module(self, module, **kwargs): # Generate C header file from module c_header = self.generate_c_header(module) self.c_path = (path/c_header.name.lower()).with_suffix('.h') - Sourcefile.to_file(source=cgen(c_header), path=self.c_path) + Sourcefile.to_file(source=self.codegen(c_header), path=self.c_path) def transform_subroutine(self, routine, **kwargs): if self.path is None: @@ -114,7 +117,7 @@ def transform_subroutine(self, routine, **kwargs): # Generate C source file from Loki IR c_kernel = self.generate_c_kernel(routine) self.c_path = (path/c_kernel.name.lower()).with_suffix('.c') - Sourcefile.to_file(source=cgen(c_kernel), path=self.c_path) + Sourcefile.to_file(source=self.codegen(c_kernel), path=self.c_path) def c_struct_typedef(self, derived): """ diff --git a/tests/test_transpile.py b/tests/test_transpile.py index 6945271da..ce37cfdd3 100644 --- a/tests/test_transpile.py +++ b/tests/test_transpile.py @@ -11,7 +11,7 @@ from conftest import jit_compile, jit_compile_lib, clean_test, available_frontends from loki import ( - Subroutine, Module, FortranCTransformation, OFP, cgen + Subroutine, Module, FortranCTransformation, OFP, cgen, cppgen ) from loki.build import Builder from loki.transform import normalize_range_indexing @@ -28,7 +28,8 @@ def fixture_builder(here): @pytest.mark.parametrize('use_c_ptr', (False, True)) @pytest.mark.parametrize('frontend', available_frontends()) -def test_transpile_simple_loops(here, builder, frontend, use_c_ptr): +@pytest.mark.parametrize('codegen', (cgen, cppgen)) +def test_transpile_simple_loops(here, builder, frontend, use_c_ptr, codegen): """ A simple test routine to test C transpilation of loops """ @@ -74,7 +75,7 @@ def test_transpile_simple_loops(here, builder, frontend, use_c_ptr): [13., 23., 33., 43.]]) # Generate and test the transpiled C kernel - f2c = FortranCTransformation(use_c_ptr=use_c_ptr) + f2c = FortranCTransformation(use_c_ptr=use_c_ptr, codegen=codegen) f2c.apply(source=routine, path=here) libname = f'fc_{routine.name}{"_c_ptr" if use_c_ptr else ""}_{frontend}' c_kernel = jit_compile_lib([f2c.wrapperpath, f2c.c_path], path=here, name=libname, builder=builder)