From 6cbe966f794e2aabbfa34ba8f302f0624884ef2d Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 21 Jan 2026 13:21:15 +0000 Subject: [PATCH 01/17] For #1823. Add Kernel symbols when Kernel is created. --- src/psyclone/psyGen.py | 13 +++ .../domain/lfric/lfric_field_codegen_test.py | 4 +- .../domain/lfric/lfric_scalar_codegen_test.py | 10 +-- src/psyclone/tests/gocean1p0_test.py | 3 +- src/psyclone/tests/lfric_lma_test.py | 4 +- src/psyclone/tests/lfric_multigrid_test.py | 6 +- src/psyclone/tests/lfric_quadrature_test.py | 10 +-- src/psyclone/tests/lfric_test.py | 2 +- src/psyclone/tests/psyGen_test.py | 79 +------------------ 9 files changed, 34 insertions(+), 97 deletions(-) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 3c9e5ee100..60e19abfe4 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1243,6 +1243,19 @@ def __init__(self, KernelArguments, call, parent=None, check=True): self._opencl_options = {'local_size': 64, 'queue_number': 1} self.arg_descriptors = call.ktype.arg_descriptors + # If we have an ancestor InvokeSchedule then add the necessary + # symbols. + invsched = self.ancestor(InvokeSchedule) + if invsched: + symtab = invsched.symbol_table + csymbol = symtab.find_or_create( + self._module_name, + symbol_type=ContainerSymbol) + _ = symtab.find_or_create( + self._name, + symbol_type=RoutineSymbol, + interface=ImportInterface(csymbol)) + def get_interface_symbol(self) -> None: ''' By default, a Kern is not polymorphic and therefore has no interface diff --git a/src/psyclone/tests/domain/lfric/lfric_field_codegen_test.py b/src/psyclone/tests/domain/lfric/lfric_field_codegen_test.py index e85a3058e7..5aa0ddc261 100644 --- a/src/psyclone/tests/domain/lfric/lfric_field_codegen_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_field_codegen_test.py @@ -317,8 +317,8 @@ def test_field_fs(tmpdir): contains subroutine invoke_0_testkern_fs_type(f1, f2, m1, m2, f3, f4, m3, m4, f5, \ f6, m5, m6, m7) - use mesh_mod, only : mesh_type use testkern_fs_mod, only : testkern_fs_code + use mesh_mod, only : mesh_type type(field_type), intent(in) :: f1 type(field_type), intent(in) :: f2 type(field_type), intent(in) :: m1 @@ -650,8 +650,8 @@ def test_int_field_fs(tmpdir): contains subroutine invoke_0_testkern_fs_int_field_type(f1, f2, m1, m2, f3, f4, m3, \ m4, f5, f6, m5, m6, f7, f8, m7) - use mesh_mod, only : mesh_type use testkern_fs_int_field_mod, only : testkern_fs_int_field_code + use mesh_mod, only : mesh_type type(integer_field_type), intent(in) :: f1 type(integer_field_type), intent(in) :: f2 type(integer_field_type), intent(in) :: m1 diff --git a/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py b/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py index f0f1dd82da..a46cea5f8d 100644 --- a/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py @@ -67,8 +67,8 @@ def test_real_scalar(tmpdir): expected = ( " subroutine invoke_0_testkern_type(a, f1, f2, m1, m2)\n" - " use mesh_mod, only : mesh_type\n" " use testkern_mod, only : testkern_code\n" + " use mesh_mod, only : mesh_type\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1\n" " type(field_type), intent(in) :: f2\n" @@ -172,9 +172,9 @@ def test_int_scalar(tmpdir): expected = ( " subroutine invoke_0_testkern_one_int_scalar_type" "(f1, iflag, f2, m1, m2)\n" - " use mesh_mod, only : mesh_type\n" " use testkern_one_int_scalar_mod, only : " "testkern_one_int_scalar_code\n" + " use mesh_mod, only : mesh_type\n" " type(field_type), intent(in) :: f1\n" " integer(kind=i_def), intent(in) :: iflag\n" " type(field_type), intent(in) :: f2\n" @@ -279,9 +279,9 @@ def test_two_real_scalars(tmpdir): expected = ( " subroutine invoke_0_testkern_two_real_scalars_type(a, f1, f2, " "m1, m2, b)\n" - " use mesh_mod, only : mesh_type\n" " use testkern_two_real_scalars_mod, only : " "testkern_two_real_scalars_code\n" + " use mesh_mod, only : mesh_type\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1\n" " type(field_type), intent(in) :: f2\n" @@ -385,9 +385,9 @@ def test_two_int_scalars(tmpdir): expected = ( " subroutine invoke_0(iflag, f1, f2, m1, m2, istep)\n" - " use mesh_mod, only : mesh_type\n" " use testkern_two_int_scalars_mod, only : " "testkern_two_int_scalars_code\n" + " use mesh_mod, only : mesh_type\n" " integer(kind=i_def), intent(in) :: iflag\n" " type(field_type), intent(in) :: f1\n" " type(field_type), intent(in) :: f2\n" @@ -511,9 +511,9 @@ def test_three_scalars(tmpdir): " contains\n" " subroutine invoke_0_testkern_three_scalars_type(a, f1, f2, m1, " "m2, lswitch, istep)\n" - " use mesh_mod, only : mesh_type\n" " use testkern_three_scalars_mod, only : " "testkern_three_scalars_code\n" + " use mesh_mod, only : mesh_type\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1\n" " type(field_type), intent(in) :: f2\n" diff --git a/src/psyclone/tests/gocean1p0_test.py b/src/psyclone/tests/gocean1p0_test.py index dd1961ceec..6d615d2579 100644 --- a/src/psyclone/tests/gocean1p0_test.py +++ b/src/psyclone/tests/gocean1p0_test.py @@ -450,7 +450,8 @@ def test_scalar_float_arg_from_module(): " end subroutine invoke_0_bc_ssh\n\n" "end module psy_single_invoke_scalar_float_test\n") - assert generated_code == expected_output + for line in expected_output.split("\n"): + assert line in generated_code, line # We don't compile this generated code as the module is made up and # the compiler would correctly fail. diff --git a/src/psyclone/tests/lfric_lma_test.py b/src/psyclone/tests/lfric_lma_test.py index d3a7385d39..accf4517c7 100644 --- a/src/psyclone/tests/lfric_lma_test.py +++ b/src/psyclone/tests/lfric_lma_test.py @@ -510,12 +510,12 @@ def test_operator_different_spaces(tmpdir): contains subroutine invoke_0_assemble_weak_derivative_w3_w2_kernel_type(mapping, \ coord, qr) + use assemble_weak_derivative_w3_w2_kernel_mod, only : \ +assemble_weak_derivative_w3_w2_kernel_code use mesh_mod, only : mesh_type use function_space_mod, only : BASIS, DIFF_BASIS use quadrature_xyoz_mod, only : quadrature_xyoz_proxy_type, \ quadrature_xyoz_type - use assemble_weak_derivative_w3_w2_kernel_mod, only : \ -assemble_weak_derivative_w3_w2_kernel_code type(operator_type), intent(in) :: mapping type(field_type), dimension(3), intent(in) :: coord type(quadrature_xyoz_type), intent(in) :: qr diff --git a/src/psyclone/tests/lfric_multigrid_test.py b/src/psyclone/tests/lfric_multigrid_test.py index 57a8311888..087b72a9f5 100644 --- a/src/psyclone/tests/lfric_multigrid_test.py +++ b/src/psyclone/tests/lfric_multigrid_test.py @@ -287,9 +287,9 @@ def test_field_prolong(tmpdir, dist_mem): assert LFRicBuild(tmpdir).code_compiles(psy) expected = ( + " use prolong_test_kernel_mod, only : prolong_test_kernel_code\n" " use mesh_mod, only : mesh_type\n" " use mesh_map_mod, only : mesh_map_type\n" - " use prolong_test_kernel_mod, only : prolong_test_kernel_code\n" " type(field_type), intent(in) :: field1\n" " type(field_type), intent(in) :: field2\n" " integer(kind=i_def) :: cell\n") @@ -387,10 +387,10 @@ def test_field_restrict(tmpdir, dist_mem, monkeypatch, annexed): assert LFRicBuild(tmpdir).code_compiles(psy) defs = ( - " use mesh_mod, only : mesh_type\n" - " use mesh_map_mod, only : mesh_map_type\n" " use restrict_test_kernel_mod, " "only : restrict_test_kernel_code\n" + " use mesh_mod, only : mesh_type\n" + " use mesh_map_mod, only : mesh_map_type\n" " type(field_type), intent(in) :: field1\n" " type(field_type), intent(in) :: field2\n") assert defs in output diff --git a/src/psyclone/tests/lfric_quadrature_test.py b/src/psyclone/tests/lfric_quadrature_test.py index 0ba897502f..63ef670abc 100644 --- a/src/psyclone/tests/lfric_quadrature_test.py +++ b/src/psyclone/tests/lfric_quadrature_test.py @@ -84,11 +84,11 @@ def test_field_xyoz(tmpdir): assert ( " subroutine invoke_0_testkern_qr_type(f1, f2, m1, a, m2, istp," " qr)\n" + " use testkern_qr_mod, only : testkern_qr_code\n" " use mesh_mod, only : mesh_type\n" " use function_space_mod, only : BASIS, DIFF_BASIS\n" " use quadrature_xyoz_mod, only : quadrature_xyoz_proxy_type, " - "quadrature_xyoz_type\n" - " use testkern_qr_mod, only : testkern_qr_code\n" in generated_code) + "quadrature_xyoz_type\n" in generated_code) assert """ type(field_type), intent(in) :: f1 type(field_type), intent(in) :: f2 @@ -294,14 +294,14 @@ def test_face_qr(tmpdir, dist_mem): " use field_mod, only : field_proxy_type, field_type\n") assert module_declns in generated_code - output_decls = "" + output_decls = (" use testkern_qr_faces_mod, only : " + "testkern_qr_faces_code\n") if dist_mem: output_decls += " use mesh_mod, only : mesh_type\n" output_decls += ( " use function_space_mod, only : BASIS, DIFF_BASIS\n" " use quadrature_face_mod, only : quadrature_face_proxy_type, " - "quadrature_face_type\n" - " use testkern_qr_faces_mod, only : testkern_qr_faces_code\n") + "quadrature_face_type\n") assert output_decls in generated_code assert """\ type(field_type), intent(in) :: f1 diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index 7bfc88dc97..3e7e228e15 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -4336,8 +4336,8 @@ def test_mixed_precision_args(tmpdir): subroutine invoke_0(scalar_r_def, field_r_def, operator_r_def, \ scalar_r_solver, field_r_solver, operator_r_solver, scalar_r_tran, \ field_r_tran, operator_r_tran, scalar_r_bl, field_r_bl) - use mesh_mod, only : mesh_type use mixed_kernel_mod, only : mixed_code + use mesh_mod, only : mesh_type real(kind=r_def), intent(in) :: scalar_r_def type(field_type), intent(in) :: field_r_def type(operator_type), intent(in) :: operator_r_def diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 8897577977..3f8ab1c926 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -548,8 +548,8 @@ def test_derived_type_deref_naming(tmpdir): output = ( " subroutine invoke_0_testkern_type" "(a, f1_my_field, f1_my_field_1, m1, m2)\n" - " use mesh_mod, only : mesh_type\n" " use testkern_mod, only : testkern_code\n" + " use mesh_mod, only : mesh_type\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1_my_field\n" " type(field_type), intent(in) :: f1_my_field_1\n" @@ -705,83 +705,6 @@ def test_codedkern_module_inline_getter_and_setter(): in str(err.value)) -def test_codedkern_module_inline_lowering(tmpdir): - ''' Check that a CodedKern with module-inline gets copied into the - local module appropriately when the PSy-layer is generated''' - # Use LFRic example with a repeated CodedKern - _, invoke_info = parse( - os.path.join(BASE_PATH, "4.6_multikernel_invokes.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=False).create(invoke_info) - invoke = psy.invokes.invoke_list[0] - schedule = invoke.schedule - coded_kern = schedule.children[0].loop_body[0] - gen = str(psy.gen) - - # Without module-inline the subroutine is used by a module import - assert "use ru_kernel_mod, only : ru_code" in gen - assert "subroutine ru_code(" not in gen - - # With module-inline the subroutine does not need to be imported - coded_kern.module_inline = True - - # Fail if local routine symbol does not already exist - with pytest.raises(VisitorError) as err: - gen = str(psy.gen) - assert ("Cannot generate this kernel call to 'ru_code' because it " - "is marked as module-inlined but no such subroutine exists in " - "this module." in str(err.value)) - - # Create the symbol and try again, it now must succeed - psy.container.symbol_table.new_symbol( - "ru_code", symbol_type=RoutineSymbol) - - gen = str(psy.gen) - assert "use ru_kernel_mod, only : ru_code" not in gen - assert LFRicBuild(tmpdir).code_compiles(psy) - - -def test_codedkern_module_inline_kernel_in_multiple_invokes(tmpdir): - ''' Check that module-inline works as expected when the same kernel - is provided in different invokes''' - # Use LFRic example with the kernel 'testkern_qr_mod' repeated once in - # the first invoke and 3 times in the second invoke. - _, invoke_info = parse( - os.path.join(BASE_PATH, "3.1_multi_functions_multi_invokes.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=False).create(invoke_info) - - # By default the kernel is imported once per invoke - gen = str(psy.gen) - assert gen.count("use testkern_qr_mod, only : testkern_qr_code") == 2 - - # Module inline kernel in invoke 1 - schedule = psy.invokes.invoke_list[0].schedule - for coded_kern in schedule.walk(CodedKern): - if coded_kern.name == "testkern_qr_code": - coded_kern.module_inline = True - # A top-level RoutineSymbol must now exist - schedule.ancestor(Container).symbol_table.new_symbol( - "testkern_qr_code", symbol_type=RoutineSymbol) - gen = str(psy.gen) - - # After this, one invoke uses the inlined top-level subroutine - # and the other imports it (shadowing the top-level symbol) - assert gen.count("use testkern_qr_mod, only : testkern_qr_code") == 1 - assert LFRicBuild(tmpdir).code_compiles(psy) - - # Module inline kernel in invoke 2 - schedule = psy.invokes.invoke_list[1].schedule - for coded_kern in schedule.walk(CodedKern): - if coded_kern.name == "testkern_qr_code": - coded_kern.module_inline = True - gen = str(psy.gen) - # After this, no imports are remaining and both use the same - # top-level implementation - assert gen.count("use testkern_qr_mod, only : testkern_qr_code") == 0 - assert LFRicBuild(tmpdir).code_compiles(psy) - - def test_codedkern_lower_to_language_level(monkeypatch): ''' Check that a generic CodedKern can be lowered to a subroutine call with the appropriate arguments''' From ba8f599f2623f4c27a63f76985d4c0bbf56f3144 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 21 Jan 2026 17:10:08 +0000 Subject: [PATCH 02/17] #1823 first sketch of mixin [skip ci] --- .../kernel_transformation_mixin.py | 22 +++++++++++++++++++ .../omp_declare_target_trans.py | 8 ++++++- src/psyclone/transformations.py | 16 +++++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 src/psyclone/domain/common/transformations/kernel_transformation_mixin.py diff --git a/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py b/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py new file mode 100644 index 0000000000..1b8386d7f7 --- /dev/null +++ b/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py @@ -0,0 +1,22 @@ +from typing import Union + +from psyclone.psyGen import CodedKern +from psyclone.psyir.nodes.node import Node +from psyclone.psyir.transformations.transformation_error import ( + TransformationError) + + +class KernelTransformationMixin: + """ """ + + def _check_kernel_is_local(self, node: Union[Node, CodedKern]): + """ """ + if not isinstance(node, CodedKern): + return + rsymbol = node.scope.symbol_table.lookup(node.name, otherwise=None) + if not rsymbol: + raise TransformationError( + f"Cannot transform this Kernel call to '{node.name}' " + f"because it is not module inlined (i.e. local to the current " + f"module. Use KernelModuleInlineTrans() first." + ) diff --git a/src/psyclone/psyir/transformations/omp_declare_target_trans.py b/src/psyclone/psyir/transformations/omp_declare_target_trans.py index 7d05b1a557..a7241fe182 100644 --- a/src/psyclone/psyir/transformations/omp_declare_target_trans.py +++ b/src/psyclone/psyir/transformations/omp_declare_target_trans.py @@ -39,13 +39,17 @@ ''' +from psyclone.domain.common.transformations.kernel_transformation_mixin \ + import KernelTransformationMixin from psyclone.psyir.nodes import OMPDeclareTargetDirective from psyclone.psyGen import Transformation, Kern from psyclone.psyir.transformations.mark_routine_for_gpu_mixin import ( MarkRoutineForGPUMixin) -class OMPDeclareTargetTrans(Transformation, MarkRoutineForGPUMixin): +class OMPDeclareTargetTrans(Transformation, + MarkRoutineForGPUMixin, + KernelTransformationMixin): ''' Adds an OpenMP declare target directive to the specified routine. @@ -146,3 +150,5 @@ def validate(self, node, options=None): super().validate(node, options=options) self.validate_it_can_run_on_gpu(node, options) + + self._check_kernel_is_local(node) diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 0315ebba53..c05e1995b7 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -53,6 +53,8 @@ from psyclone.core import Signature, VariablesAccessMap from psyclone.domain.lfric import (KernCallArgList, LFRicConstants, LFRicInvokeSchedule, LFRicKern, LFRicLoop) +from psyclone.domain.common.transformations.kernel_transformation_mixin \ + import KernelTransformationMixin from psyclone.lfric import LFRicHaloExchangeEnd, LFRicHaloExchangeStart from psyclone.errors import InternalError from psyclone.gocean1p0 import GOInvokeSchedule @@ -1687,7 +1689,7 @@ def validate(self, node, options): f"'{type(node)}'.") -class LFRicKernelConstTrans(Transformation): +class LFRicKernelConstTrans(Transformation, KernelTransformationMixin): '''Modifies a kernel so that the number of dofs, number of layers and number of quadrature points are fixed in the kernel rather than being passed in by argument. @@ -1967,6 +1969,8 @@ def validate(self, node, options=None): f"Error in LFRicKernelConstTrans transformation. Supplied " f"node must be an LFRic kernel but found '{type(node)}'.") + self._check_kernel_is_local(node) + if not options: options = {} cellshape = options.get("cellshape", "quadrilateral") @@ -2178,7 +2182,9 @@ def validate(self, sched, options={}): self.check_child_async(sched, async_queue) -class ACCRoutineTrans(Transformation, MarkRoutineForGPUMixin): +class ACCRoutineTrans(Transformation, + MarkRoutineForGPUMixin, + KernelTransformationMixin): ''' Transform a kernel or routine by adding a "!$acc routine" directive (causing it to be compiled for the OpenACC accelerator device). @@ -2274,6 +2280,8 @@ def validate(self, node, options=None): self.validate_it_can_run_on_gpu(node, options) + self._check_kernel_is_local(node) + if options and "parallelism" in options: para = options["parallelism"] if para not in ACCRoutineDirective.SUPPORTED_PARALLELISM: @@ -2423,7 +2431,7 @@ def validate(self, nodes, options): f"component is the one being iterated over.") -class KernelImportsToArguments(Transformation): +class KernelImportsToArguments(Transformation, KernelTransformationMixin): ''' Transformation that removes any accesses of imported data from the supplied kernel and places them in the caller. The values/references are then passed @@ -2472,6 +2480,8 @@ def validate(self, node, options=None): f"for the GOcean API but got an InvokeSchedule of type: " f"'{type(invoke_schedule).__name__}'") + self._check_kernel_is_local(node) + # Check that there are no unqualified imports or undeclared symbols try: kernels = node.get_callees() From 5b4b68ba452490bf0d4fbab6085576b744bdbad4 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 21 Jan 2026 19:25:32 +0000 Subject: [PATCH 03/17] #1823 WIP fixing module-inlining for multiple kernel calls [skip ci] --- .../kernel_transformation_mixin.py | 4 +- src/psyclone/psyGen.py | 153 ++---------------- .../kernel_module_inline_trans_test.py | 35 ---- .../kernel_transformation_test.py | 21 +-- 4 files changed, 20 insertions(+), 193 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py b/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py index 1b8386d7f7..4f4eb2aff4 100644 --- a/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py +++ b/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py @@ -14,9 +14,9 @@ def _check_kernel_is_local(self, node: Union[Node, CodedKern]): if not isinstance(node, CodedKern): return rsymbol = node.scope.symbol_table.lookup(node.name, otherwise=None) - if not rsymbol: + if not rsymbol or rsymbol.is_import or rsymbol.is_unresolved: raise TransformationError( f"Cannot transform this Kernel call to '{node.name}' " f"because it is not module inlined (i.e. local to the current " - f"module. Use KernelModuleInlineTrans() first." + f"module). Use KernelModuleInlineTrans() first." ) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 60e19abfe4..c2a919db00 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1399,36 +1399,16 @@ def lower_to_language_level(self): ''' symtab = self.ancestor(InvokeSchedule).symbol_table - if not self.module_inline: - # If it is not module inlined then make sure we generate the kernel - # file (and rename it when necessary). - self.rename_and_write() - # Then find or create the imported RoutineSymbol - try: - # Limit scope to this Invoke, since a kernel with the same name - # may have been inlined from another invoke in the same file, - # but we have it here marked as "not module-inlined" - rsymbol = symtab.lookup(self._name, scope_limit=symtab.node) - except KeyError: - csymbol = symtab.find_or_create( - self._module_name, - symbol_type=ContainerSymbol) - rsymbol = symtab.new_symbol( - self._name, - symbol_type=RoutineSymbol, - # And allow shadowing in case it is also inlined with - # the same name by another invoke - shadowing=True, - interface=ImportInterface(csymbol)) - else: - # If it's inlined, the symbol must exist - try: - rsymbol = self.scope.symbol_table.lookup(self._name) - except KeyError as err: - raise GenerationError( - f"Cannot generate this kernel call to '{self.name}' " - f"because it is marked as module-inlined but no such " - f"subroutine exists in this module.") from err + # Limit scope to this Invoke, since a kernel with the same name + # may have been inlined from another invoke in the same file, + # but we have it here marked as "not module-inlined" + rsymbol = symtab.lookup(self._name, # scope_limit=symtab.node, + otherwise=None) + if not rsymbol: + import pdb; pdb.set_trace() + raise GenerationError( + f"Cannot lower this Kernel call to '{self.name}' " + f"because no corresponding Symbol exists in this module.") # Create Call to the rsymbol with the argument expressions as children # of the new node @@ -1495,119 +1475,6 @@ def _new_name(original, tag, suffix): return original[:-len(suffix)] + tag + suffix return original + tag + suffix - def rename_and_write(self): - ''' - Writes the (transformed) AST of this kernel to file and resets the - 'modified' flag to False. By default (config.kernel_naming == - "multiple"), the kernel is re-named so as to be unique within - the kernel output directory stored within the configuration - object. Alternatively, if config.kernel_naming is "single" - then no re-naming and output is performed if there is already - a transformed copy of the kernel in the output dir. (In this - case a check is performed that the transformed kernel already - present is identical to the one that we would otherwise write - to file. If this is not the case then we raise a GenerationError.) - - :raises GenerationError: if config.kernel_naming == "single" and a \ - different, transformed version of this \ - kernel is already in the output directory. - :raises NotImplementedError: if the kernel has been transformed but \ - is also flagged for module-inlining. - - ''' - from psyclone.line_length import FortLineLength - - config = Config.get() - - # If this kernel has not been transformed we do nothing, also if the - # kernel has been module-inlined, the routine already exist in the - # PSyIR and we don't need to generate a new file with it. - if not self.modified or self.module_inline: - return - - # Remove any "_mod" if the file follows the PSyclone naming convention - orig_mod_name = self.module_name[:] - if orig_mod_name.lower().endswith("_mod"): - old_base_name = orig_mod_name[:-4] - else: - old_base_name = orig_mod_name[:] - - # We could create a hash of a string built from the name of the - # Algorithm (module), the name/position of the Invoke and the - # index of this kernel within that Invoke. However, that creates - # a very long name so we simply ensure that kernel names are unique - # within the user-supplied kernel-output directory. - name_idx = -1 - fdesc = None - while not fdesc: - name_idx += 1 - new_suffix = "" - - new_suffix += f"_{name_idx}" - new_name = old_base_name + new_suffix + "_mod.f90" - - try: - # Atomically attempt to open the new kernel file (in case - # this is part of a parallel build) - fdesc = os.open( - os.path.join(config.kernel_output_dir, new_name), - os.O_CREAT | os.O_WRONLY | os.O_EXCL) - except (OSError, IOError): - # The os.O_CREATE and os.O_EXCL flags in combination mean - # that open() raises an error if the file exists - if config.kernel_naming == "single": - # If the kernel-renaming scheme is such that we only ever - # create one copy of a transformed kernel then we're done - break - continue - - # Use the suffix we have determined to rename all relevant quantities - # within the AST of the kernel code. - self._rename_psyir(new_suffix) - - # Kernel is now self-consistent so unset the modified flag - self.modified = False - - # If we reach this point the kernel needs to be written out into a - # file using a PSyIR back-end. At the moment there is no way to choose - # which back-end to use, so simply use the Fortran one (and limit the - # line length). - fortran_writer = FortranWriter( - check_global_constraints=config.backend_checks_enabled) - # Start from the root of the schedule as we want to output - # any module information surrounding the kernel subroutine - # as well as the subroutine itself. - schedules = self.get_callees() - new_kern_code = fortran_writer(schedules[0].root) - fll = FortLineLength() - new_kern_code = fll.process(new_kern_code) - - if not fdesc: - # If we've not got a file descriptor at this point then that's - # because the file already exists and the kernel-naming scheme - # ("single") means we're not creating a new one. - # Check that what we've got is the same as what's in the file - with open(os.path.join(config.kernel_output_dir, - new_name), "r") as ffile: - kern_code = ffile.read() - if kern_code != new_kern_code: - raise GenerationError( - f"A transformed version of this Kernel " - f"'{self._module_name + '''.f90'''}' already exists " - f"in the kernel-output directory " - f"({config.kernel_output_dir}) but is not the " - f"same as the current, transformed kernel and the " - f"kernel-renaming scheme is set to " - f"'{config.kernel_naming}'. (If you wish to" - f" generate a new, unique kernel for every kernel " - f"that is transformed then use " - f"'--kernel-renaming multiple'.)") - else: - # Write the modified AST out to file - os.write(fdesc, new_kern_code.encode()) - # Close the new kernel file - os.close(fdesc) - def _rename_psyir(self, suffix): '''Rename the PSyIR module and kernel names by adding the supplied suffix to the names. This change affects the KernCall and diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index 479c2bc13c..e91eec1043 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -210,41 +210,6 @@ def test_validate_no_inline_global_var(parser): inline_trans.validate(kernels[0]) -def test_validate_name_clashes(): - ''' Test that if the module-inline transformation finds the kernel name - already used in the Container scope, it raises the appropriate error''' - # Use LFRic example with a repeated CodedKern - psy, _ = get_invoke("4.6_multikernel_invokes.f90", "lfric", idx=0, - dist_mem=False) - schedule = psy.invokes.invoke_list[0].schedule - coded_kern = schedule.children[0].loop_body[0] - inline_trans = KernelModuleInlineTrans() - - # Check that name clashes which are not subroutines are detected - schedule.symbol_table.add(DataSymbol("ru_code", REAL_TYPE)) - with pytest.raises(TransformationError) as err: - inline_trans.apply(coded_kern) - assert ("Cannot module-inline Kernel 'ru_code' because symbol " - "'ru_code: DataSymbol, Automatic>' with " - "the same name already exists and changing the name of " - "module-inlined subroutines is not supported yet." - in str(err.value)) - - # TODO # 898. Manually force removal of previous imported symbol - # symbol_table.remove() is not implemented yet. - schedule.symbol_table._symbols.pop("ru_code") - - # Check that if a subroutine with the same name already exists and it is - # not identical, it fails. - schedule.parent.addchild(Routine.create("ru_code")) - with pytest.raises(TransformationError) as err: - inline_trans.apply(coded_kern) - assert ("Kernel 'ru_code' cannot be module inlined into Container " - "'multikernel_invokes_7_psy' because a *different* routine with " - "that name already exists and versioning of module-inlined " - "subroutines is not implemented yet.") in str(err.value) - - def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): ''' Test that the validate method refuses to transform a kernel which contains local variables that shadow a module name that would need to diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index 8924f5e913..fd65424aa1 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -44,6 +44,7 @@ import pytest from psyclone.configuration import Config +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.lfric.lfric_builtins import LFRicBuiltIn from psyclone.generator import GenerationError from psyclone.psyGen import Kern @@ -555,23 +556,17 @@ def test_2kern_trans(kernel_outputdir): kernels = sched.walk(Kern) assert len(kernels) == 5 ktrans = LFRicKernelConstTrans() + mod_inline = KernelModuleInlineTrans() + mod_inline.apply(kernels[1]) ktrans.apply(kernels[1], {"number_of_layers": 100}) + mod_inline.apply(kernels[2]) ktrans.apply(kernels[2], {"number_of_layers": 100}) - # Generate the code (this triggers the generation of new kernels) + # Generate the code. code = str(psy.gen).lower() - # Find the tags added to the kernel/module names - for match in re.finditer('use testkern_any_space_2(.+?)_mod', code): - tag = match.group(1) - assert (f"use testkern_any_space_2{tag}_mod, only : " - f"testkern_any_space_2{tag}_code" in code) - assert f"call testkern_any_space_2{tag}_code(" in code - filepath = os.path.join(str(kernel_outputdir), - f"testkern_any_space_2{tag}_mod.f90") - assert os.path.isfile(filepath) - with open(filepath, encoding="utf-8") as infile: - assert "nlayers = 100" in infile.read() + # Check that the old module re-naming no longer happens. + assert not re.match('use testkern_any_space_2(.+?)_mod', code) assert "use testkern_any_space_2_mod, only" not in code - assert "call testkern_any_space_2_code(" not in code + assert "call testkern_any_space_2_code(" in code assert LFRicBuild(kernel_outputdir).code_compiles(psy) From 95b57102547aa80517b5499e7622acd700088828 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 22 Jan 2026 14:00:36 +0000 Subject: [PATCH 04/17] #1823 WIP removing .module_inline setter and fixing tests [skip ci] --- .../kernel_module_inline_trans.py | 7 +- src/psyclone/domain/lfric/lfric_kern.py | 2 + src/psyclone/psyGen.py | 77 +++++++------------ .../kernel_module_inline_trans_test.py | 4 +- src/psyclone/tests/psyGen_test.py | 43 ----------- 5 files changed, 37 insertions(+), 96 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index f7288441e6..aa1ee92c6d 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -51,7 +51,7 @@ ContainerSymbol, ImportInterface, GenericInterfaceSymbol, RoutineSymbol, Symbol, SymbolError, SymbolTable) from psyclone.psyir.nodes import ( - Call, Container, FileContainer, Routine, ScopingNode, + Call, Container, FileContainer, Reference, Routine, ScopingNode, IntrinsicCall, ) from psyclone.utils import transformation_documentation_wrapper @@ -620,6 +620,9 @@ def apply(self, node, options=None, **kwargs): sym = node.scope.symbol_table.lookup(external_callee_name) table = sym.find_symbol_table(node) table.rename_symbol(sym, caller_name) + new_sym = sym + else: + new_sym = node.scope.symbol_table.lookup(caller_name) # Update the Kernel to point to the updated PSyIR and set # the module-inline flag to avoid generating the kernel imports @@ -637,6 +640,6 @@ def apply(self, node, options=None, **kwargs): # point to the module-inlined version. for kern in cntr.walk(CodedKern, stop_type=CodedKern): if kern.name == node.name: - kern.module_inline = True # pylint: disable=protected-access kern._schedules = updated_routines + kern.routine = Reference(new_sym) diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index f89d107c92..04082ed1a0 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -838,6 +838,8 @@ class creates the PSyIR schedule(s) on first invocation which is then for name in names: rt_psyir = container.find_routine_psyir(name, allow_private=True) + if not rt_psyir: + import pdb; pdb.set_trace() routines.append(rt_psyir) # Otherwise, get the PSyIR Kernel Schedule(s) from the original diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index c2a919db00..c8c07ccb9a 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -41,12 +41,13 @@ and generation. The classes in this method need to be specialised for a particular API and implementation. ''' +from __future__ import annotations from dataclasses import dataclass import inspect import os from collections import OrderedDict import abc -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import warnings try: @@ -58,7 +59,7 @@ from psyclone.configuration import Config, LFRIC_API_NAMES, GOCEAN_API_NAMES from psyclone.core import AccessType from psyclone.errors import GenerationError, InternalError, FieldNotFoundError -from psyclone.parse.algorithm import BuiltInCall +from psyclone.parse.algorithm import BuiltInCall, KernelCall from psyclone.psyir.backend.fortran import FortranWriter from psyclone.psyir.nodes import ( ArrayReference, Call, Container, Literal, Loop, Node, OMPDoDirective, @@ -1207,14 +1208,11 @@ class CodedKern(Kern): Class representing a call to a PSyclone Kernel with a user-provided implementation. The kernel may or may not be in-lined. - :param type KernelArguments: the API-specific sub-class of \ - :py:class:`psyclone.psyGen.Arguments` to \ - create. + :param KernelArguments: the API-specific sub-class of + :py:class:`psyclone.psyGen.Arguments` to create. :param call: Details of the call to this kernel in the Algorithm layer. - :type call: :py:class:`psyclone.parse.algorithm.KernelCall`. :param parent: the parent of this Node (kernel call) in the Schedule. - :type parent: sub-class of :py:class:`psyclone.psyir.nodes.Node`. - :param bool check: whether to check for consistency between the \ + :param check: whether to check for consistency between the kernel metadata and the algorithm layer. Defaults to True. ''' @@ -1222,14 +1220,18 @@ class CodedKern(Kern): _text_name = "CodedKern" _colour = "magenta" - def __init__(self, KernelArguments, call, parent=None, check=True): + def __init__(self, + KernelArguments: type, + call: KernelCall, + parent: Node = None, + check: bool = True): # Set module_name first in case there is an error when # processing arguments, as we can then return the module_name # from where it happened. self._module_name = call.module_name - super(CodedKern, self).__init__(parent, call, - call.ktype.procedure.name, - KernelArguments, check) + super().__init__(parent, call, + call.ktype.procedure.name, + KernelArguments, check) self._module_code = call.ktype._ast self._kernel_code = call.ktype.procedure self._fp2_ast = None #: The fparser2 AST for the kernel @@ -1237,24 +1239,25 @@ def __init__(self, KernelArguments, call, parent=None, check=True): self._schedules = None #: Whether or not this kernel has been transformed self._modified = False - #: Whether or not to in-line this kernel into the module containing - #: the PSy layer - self._module_inline = False self._opencl_options = {'local_size': 64, 'queue_number': 1} self.arg_descriptors = call.ktype.arg_descriptors # If we have an ancestor InvokeSchedule then add the necessary # symbols. - invsched = self.ancestor(InvokeSchedule) - if invsched: - symtab = invsched.symbol_table + # TODO #2054 - this 'routine' property can be replaced once this + # class sub-classes Call. + self.routine: Optional[Reference] = None + container = self.ancestor(Container) + if container: + symtab = container.symbol_table csymbol = symtab.find_or_create( self._module_name, symbol_type=ContainerSymbol) - _ = symtab.find_or_create( + rsymbol = symtab.find_or_create( self._name, symbol_type=RoutineSymbol, interface=ImportInterface(csymbol)) + self.routine = Reference(rsymbol) def get_interface_symbol(self) -> None: ''' @@ -1341,38 +1344,14 @@ def dag_name(self): return f"kernel_{self.name}_{position}" @property - def module_inline(self): + def module_inline(self) -> bool: ''' :returns: whether or not this kernel is being module-inlined. - :rtype: bool - ''' - return self._module_inline - - @module_inline.setter - def module_inline(self, value): ''' - Setter for whether or not to module-inline this kernel. - - :param bool value: whether or not to module-inline this kernel. - ''' - if value is not True: - raise TypeError( - f"The module inline parameter only accepts the type boolean " - f"'True' since module-inlining is irreversible. But found:" - f" '{value}'.") - # Do the same to all kernels in this invoke with the same name. - # This is needed because lowering would otherwise add - # an import with the same name and shadow the module-inline routine - # symbol. - # TODO 1823: The transformation could have more control about this by - # giving an option to specify if the module-inline applies to a - # single kernel, the whole invoke or the whole algorithm. - my_schedule = self.ancestor(InvokeSchedule) - for kernel in my_schedule.walk(Kern): - if kernel is self: - self._module_inline = value - elif kernel.name == self.name and kernel.module_inline != value: - kernel.module_inline = value + if (not self.routine or self.routine.symbol.is_import or + self.routine.symbol.is_unresolved): + return False + return True def node_str(self, colour=True): ''' Returns the name of this node with (optional) control codes @@ -1385,7 +1364,7 @@ def node_str(self, colour=True): ''' return (self.coloured_name(colour) + " " + self.name + "(" + self.arguments.names + ") " + "[module_inline=" + - str(self._module_inline) + "]") + str(self.module_inline) + "]") def lower_to_language_level(self): ''' diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index e91eec1043..e85d8bce70 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -443,9 +443,9 @@ def test_module_inline_apply_kernel_in_multiple_invokes(tmpdir): psy, _ = get_invoke("3.1_multi_functions_multi_invokes.f90", "lfric", idx=0, dist_mem=False) - # By default the kernel is imported once per invoke + # By default the kernel is imported just once (in the outer container) gen = str(psy.gen) - assert gen.count("use testkern_qr_mod, only : testkern_qr_code") == 2 + assert gen.count("use testkern_qr_mod, only : testkern_qr_code") == 1 assert gen.count("end subroutine testkern_qr_code") == 0 # Module inline kernel in invoke 1 diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index 3f8ab1c926..bb58ab4b83 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -662,49 +662,6 @@ def test_codedkern_node_str(): assert expected_output in out -def test_codedkern_module_inline_getter_and_setter(): - ''' Check that the module_inline setter changes the module inline - attribute to all the same kernels in the invoke''' - # Use LFRic example with a repeated CodedKern - _, invoke_info = parse( - os.path.join(BASE_PATH, "4.6_multikernel_invokes.f90"), - api="lfric") - psy = PSyFactory("lfric", distributed_memory=False).create(invoke_info) - invoke = psy.invokes.invoke_list[0] - schedule = invoke.schedule - coded_kern_1 = schedule.children[0].loop_body[0] - coded_kern_2 = schedule.children[1].loop_body[0] - - # By default they are not module-inlined - assert not coded_kern_1.module_inline - assert not coded_kern_2.module_inline - assert "module_inline=False" in coded_kern_1.node_str() - assert "module_inline=False" in coded_kern_2.node_str() - - # It can be turned on (and both kernels change) - coded_kern_1.module_inline = True - assert coded_kern_1.module_inline - assert coded_kern_2.module_inline - assert "module_inline=True" in coded_kern_1.node_str() - assert "module_inline=True" in coded_kern_2.node_str() - - # It can not be turned off - with pytest.raises(TypeError) as err: - coded_kern_2.module_inline = False - assert ("The module inline parameter only accepts the type boolean " - "'True' since module-inlining is irreversible. But found: 'False'" - in str(err.value)) - assert coded_kern_1.module_inline - assert coded_kern_2.module_inline - - # And it doesn't accept other types - with pytest.raises(TypeError) as err: - coded_kern_2.module_inline = 3 - assert ("The module inline parameter only accepts the type boolean " - "'True' since module-inlining is irreversible. But found: '3'" - in str(err.value)) - - def test_codedkern_lower_to_language_level(monkeypatch): ''' Check that a generic CodedKern can be lowered to a subroutine call with the appropriate arguments''' From 1bb59ebcffbafb707a865b9e85c729fa7c42d05a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 23 Jan 2026 14:36:41 +0000 Subject: [PATCH 05/17] #1823 fix module-inline tests --- src/psyclone/domain/lfric/lfric_kern.py | 5 ++--- src/psyclone/psyGen.py | 10 ++-------- .../transformations/kernel_module_inline_trans_test.py | 9 +++++---- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py index 04082ed1a0..c3d1d01a0c 100644 --- a/src/psyclone/domain/lfric/lfric_kern.py +++ b/src/psyclone/domain/lfric/lfric_kern.py @@ -838,9 +838,8 @@ class creates the PSyIR schedule(s) on first invocation which is then for name in names: rt_psyir = container.find_routine_psyir(name, allow_private=True) - if not rt_psyir: - import pdb; pdb.set_trace() - routines.append(rt_psyir) + if rt_psyir: + routines.append(rt_psyir) # Otherwise, get the PSyIR Kernel Schedule(s) from the original # parse tree. diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index c8c07ccb9a..80e902ae02 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1366,25 +1366,19 @@ def node_str(self, colour=True): self.arguments.names + ") " + "[module_inline=" + str(self.module_inline) + "]") - def lower_to_language_level(self): + def lower_to_language_level(self) -> Node: ''' In-place replacement of CodedKern concept into language level PSyIR constructs. The CodedKern is implemented as a Call to a routine with the appropriate arguments. :returns: the lowered version of this node. - :rtype: :py:class:`psyclone.psyir.node.Node` ''' symtab = self.ancestor(InvokeSchedule).symbol_table - # Limit scope to this Invoke, since a kernel with the same name - # may have been inlined from another invoke in the same file, - # but we have it here marked as "not module-inlined" - rsymbol = symtab.lookup(self._name, # scope_limit=symtab.node, - otherwise=None) + rsymbol = symtab.lookup(self._name, otherwise=None) if not rsymbol: - import pdb; pdb.set_trace() raise GenerationError( f"Cannot lower this Kernel call to '{self.name}' " f"because no corresponding Symbol exists in this module.") diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index e85d8bce70..f5c412f1c6 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -281,13 +281,15 @@ def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): monkeypatch.setattr(kern_call, "_schedules", [routine]) container = kern_call.ancestor(Container) - assert "compute_cv_code" not in container.symbol_table + rsym = container.symbol_table.lookup("compute_cv_code") + assert rsym.is_import inline_trans.apply(kern_call) - # A RoutineSymbol should have been added to the Container symbol table. + # The RoutineSymbol should no longer be an import. rsym = container.symbol_table.lookup("compute_cv_code") assert isinstance(rsym, RoutineSymbol) + assert not rsym.is_import assert rsym.visibility == Symbol.Visibility.PRIVATE @@ -502,8 +504,7 @@ def test_module_inline_apply_polymorphic_kernel_in_multiple_invokes(tmpdir): operator_r_def, f1, f2, m1, a, m2, istp, qr) use function_space_mod, only : basis, diff_basis use quadrature_xyoz_mod, only : quadrature_xyoz_proxy_type, \ -quadrature_xyoz_type - use testkern_qr_mod, only : testkern_qr_code""" in output) +quadrature_xyoz_type""" in output) assert "mixed_kernel_mod" not in output assert LFRicBuild(tmpdir).code_compiles(psy) From 38118938924027377e623ff02802f61671b4bba1 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 23 Jan 2026 16:39:40 +0000 Subject: [PATCH 06/17] #1823 WIP fixing tests [skip ci] --- .../kernel_transformation_mixin.py | 24 +++- .../transformations/globalstoargs_test.py | 106 ++++++------------ src/psyclone/transformations.py | 27 +---- 3 files changed, 55 insertions(+), 102 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py b/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py index 4f4eb2aff4..6c9631ee9f 100644 --- a/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py +++ b/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py @@ -1,6 +1,7 @@ from typing import Union from psyclone.psyGen import CodedKern +from psyclone.psyir.nodes.container import Container from psyclone.psyir.nodes.node import Node from psyclone.psyir.transformations.transformation_error import ( TransformationError) @@ -13,10 +14,27 @@ def _check_kernel_is_local(self, node: Union[Node, CodedKern]): """ """ if not isinstance(node, CodedKern): return + + msg_text = (f"Cannot transform this Kernel call to '{node.name}' " + f"because") + rsymbol = node.scope.symbol_table.lookup(node.name, otherwise=None) if not rsymbol or rsymbol.is_import or rsymbol.is_unresolved: raise TransformationError( - f"Cannot transform this Kernel call to '{node.name}' " - f"because it is not module inlined (i.e. local to the current " - f"module). Use KernelModuleInlineTrans() first." + f"{msg_text} it is not module inlined (i.e. local to the " + f"current module). Use KernelModuleInlineTrans() first." + ) + container = node.ancestor(Container) + if not container: + raise TransformationError( + f"{msg_text} because there is no ancestor Container in which " + f"to look for its implementation." ) + names = container.resolve_routine(node.name) + for name in names: + rt = container.find_routine_psyir(name, allow_private=True) + if not rt: + raise TransformationError( + f"{msg_text} the ancestor Container does not contain " + f"a Routine named '{name}'" + ) diff --git a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py index 3f7984f218..9ba53a9d28 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -38,7 +38,9 @@ import os import pytest +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.gocean1p0 import GOKern +from psyclone.parse import ModuleManager from psyclone.parse.algorithm import parse from psyclone.psyGen import PSyFactory, InvokeSchedule from psyclone.psyir.symbols import (DataSymbol, REAL_TYPE, INTEGER_TYPE, @@ -71,9 +73,8 @@ def test_kernelimportstoargumentstrans_wrongapi(): "type:" in str(err.value) -def test_kernelimportsstoargumentstrans_no_outer_module_import(): - ''' Check that we reject kernels that access data that is declared in the - enclosing module. ''' +def test_kernelimportsstoargumentstrans_requires_module_inline(): + ''' Check that we reject kernels that have not been module inlined. ''' trans = KernelImportsToArguments() path = os.path.join(BASEPATH, "gocean1p0") _, invoke_info = parse(os.path.join(path, @@ -84,76 +85,28 @@ def test_kernelimportsstoargumentstrans_no_outer_module_import(): kernel = invoke.schedule.coded_kernels()[0] with pytest.raises(TransformationError) as err: trans.validate(kernel) - assert ("contains accesses to 'alpha' which is declared in the callee " - "module scope." in str(err.value)) + assert ("Cannot transform this Kernel call to 'kernel_with_global_code' " + "because it is not module inlined" in str(err.value)) -def test_kernelimportsstoargumentstrans_unsuccessful_get_callees(monkeypatch): - ''' Check that the validation produces the correct error when a - get_callees method is unsuccessful. - ''' - trans = KernelImportsToArguments() - path = os.path.join(BASEPATH, "gocean1p0") - _, invoke_info = parse(os.path.join(path, - "single_invoke_kern_with_global.f90"), - api=API) - psy = PSyFactory(API).create(invoke_info) - invoke = psy.invokes.invoke_list[0] - kernel = invoke.schedule.coded_kernels()[0] - - # Monkeypatch get_callees to always produce an error. - def raise_error(_): - raise SymbolError("some error") - - monkeypatch.setattr(GOKern, "get_callees", raise_error) - with pytest.raises(TransformationError) as err: - trans.validate(kernel) - assert ("Kernel 'kernel_with_global_code' contains undeclared symbol:" - in str(err.value)) - - -def test_kernelimportstoargumentstrans_no_wildcard_import(): - ''' Check that the transformation rejects kernels with wildcard - imports. ''' - trans = KernelImportsToArguments() - psy, invoke_info = get_invoke( - "single_invoke_kern_with_unqualified_use.f90", idx=0, api=API) - kernel = invoke_info.schedule.coded_kernels()[0] - with pytest.raises(TransformationError) as err: - trans.apply(kernel) - assert ("'kernel_with_use_code' contains accesses to 'rdt' which is " - "unresolved" in str(err.value)) - - -@pytest.mark.xfail(reason="Transformation does not set modified property " - "of kernel - #663") -@pytest.mark.usefixtures("kernel_outputdir") -def test_kernelimportstoargumentstrans(monkeypatch): +def test_kernelimportstoargumentstrans(monkeypatch, fortran_writer): ''' Check the KernelImportsToArguments transformation with a single kernel invoke and an imported variable.''' from psyclone.psyGen import Argument - from psyclone.psyir.backend.fortran import FortranWriter trans = KernelImportsToArguments() assert trans.name == "KernelImportsToArguments" - assert str(trans) == "Convert the imported variables used inside the " \ - "kernel into arguments and modify the InvokeSchedule to pass them" \ - " in the kernel call." + assert str(trans) == ("Convert the imported variables used inside the " + "kernel into arguments and modify the InvokeSchedule" + " to pass them in the kernel call.") # Construct a testing InvokeSchedule - _, invoke_info = parse(os.path.join(BASEPATH, "gocean1p0", - "single_invoke_kern_with_use.f90"), - api=API) - psy = PSyFactory(API).create(invoke_info) - invoke = psy.invokes.invoke_list[0] + psy, invoke = get_invoke("single_invoke_kern_with_use.f90", api=API, idx=0) notkernel = invoke.schedule.children[0] kernel = invoke.schedule.coded_kernels()[0] - # Monkeypatch resolve_type to avoid module searching and importing - # in this test. In this case we assume it is a REAL - def set_to_real(variable): - variable._datatype = REAL_TYPE - monkeypatch.setattr(DataSymbol, "resolve_type", set_to_real) + mman = ModuleManager.get() + mman.add_search_path(BASEPATH) # Test with invalid node with pytest.raises(TransformationError) as err: @@ -162,6 +115,9 @@ def set_to_real(variable): " to CodedKern nodes but found 'GOLoop' instead." in str(err.value)) + # Kernel has to be module-inlined first. + KernelModuleInlineTrans().apply(kernel) + # Test transforming a single kernel trans.apply(kernel) @@ -169,8 +125,6 @@ def set_to_real(variable): # The transformation; # 1) Has imported the symbol into the InvokeSchedule - assert invoke.schedule.symbol_table.lookup("rdt") - assert invoke.schedule.symbol_table.lookup("model_mod") var = invoke.schedule.symbol_table.lookup("rdt") container = invoke.schedule.symbol_table.lookup("model_mod") assert var.is_import @@ -178,30 +132,34 @@ def set_to_real(variable): # 2) Has added the symbol as the last argument in the kernel call assert isinstance(kernel.args[-1], Argument) - assert kernel.args[-1].name == "rdt" + assert kernel.args[-1].name == "magic" + assert kernel.args[-2].name == "rdt" # 3) Has converted the Kernel Schedule symbol into an argument which is # in also the last position - ksymbol = kernel.get_callees()[0].symbol_table.lookup("rdt") + routine = kernel.get_callees()[0] + ksymbol = routine.symbol_table.lookup("rdt") assert ksymbol.is_argument - assert kernel.get_callees()[0].symbol_table.argument_list[-1] == \ - ksymbol + assert routine.symbol_table.argument_list[-2] is ksymbol + ksym2 = routine.symbol_table.lookup("magic") + assert ksym2.is_argument + assert routine.symbol_table.argument_list[-1] is ksym2 + assert len(kernel.get_callees()[0].symbol_table.argument_list) == \ len(kernel.args) + 2 # GOcean kernels have 2 implicit arguments # Check the kernel code is generated as expected - fwriter = FortranWriter() - kernel_code = fwriter(kernel.get_callees()[0]) - assert "subroutine kernel_with_use_code(ji,jj,istep,ssha,tmask,rdt)" \ - in kernel_code - assert "real, intent(inout) :: rdt" in kernel_code + kernel_code = fortran_writer(kernel.get_callees()[0]) + assert "subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, rdt, magic)" in kernel_code + assert "real(kind=go_wp), intent(in) :: rdt" in kernel_code + assert "real(kind=go_wp), intent(inout) :: magic" in kernel_code # Check that the PSy-layer generated code now contains the use statement # and argument call generated_code = str(psy.gen) - assert "use model_mod, only : rdt" in generated_code - assert "call kernel_with_use_code(i, j, oldu_fld, cu_fld%data, " \ - "cu_fld%grid%tmask, rdt)" in generated_code + assert "use model_mod, only : magic, rdt" in generated_code + assert ("call kernel_with_use_code(i, j, oldu_fld, cu_fld%data, " + "cu_fld%grid%tmask, rdt, magic)" in generated_code) assert invoke.schedule.symbol_table.lookup("model_mod") assert invoke.schedule.symbol_table.lookup("rdt") diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index c05e1995b7..ba51d112df 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -2462,11 +2462,6 @@ def validate(self, node, options=None): :raises TransformationError: if the supplied node is not a CodedKern. :raises TransformationError: if this transformation is not applied to a Gocean API Invoke. - :raises TransformationError: if the supplied node is a polymorphic - Kernel. - :raises TransformationError: if the supplied kernel contains wildcard - imports of symbols from one or more containers (e.g. a USE without - an ONLY clause in Fortran). ''' if not isinstance(node, CodedKern): raise TransformationError( @@ -2480,28 +2475,10 @@ def validate(self, node, options=None): f"for the GOcean API but got an InvokeSchedule of type: " f"'{type(invoke_schedule).__name__}'") + # Check that the kernel has already been module-inlined. This also + # implies that there are no unqualified imports or undeclared symbols. self._check_kernel_is_local(node) - # Check that there are no unqualified imports or undeclared symbols - try: - kernels = node.get_callees() - except (SymbolError, NotImplementedError) as err: - raise TransformationError( - f"Kernel '{node.name}' contains undeclared symbol: " - f"{err.value}") from err - - for kernel in kernels: - try: - kernel.check_outer_scope_accesses( - node, "Kernel", - permit_unresolved=False, - ignore_non_data_accesses=True) - except SymbolError as err: - raise TransformationError( - f"Cannot apply {self.name} to Kernel '{node.name}' " - f"because it accesses data from its outer scope: " - f"{err.value}") from err - def apply(self, node, options=None): ''' Convert the imported variables used inside the kernel into arguments From 3082aa874c86f49cddea8045ccdada53c84db398 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 26 Jan 2026 10:06:39 +0000 Subject: [PATCH 07/17] #1823 WIP fixing more tests [skip ci] --- .../transformations/globalstoargs_test.py | 86 ++++++++----------- src/psyclone/transformations.py | 7 +- 2 files changed, 39 insertions(+), 54 deletions(-) diff --git a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py index 9ba53a9d28..34f4f874b7 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -39,12 +39,11 @@ import os import pytest from psyclone.domain.common.transformations import KernelModuleInlineTrans -from psyclone.gocean1p0 import GOKern from psyclone.parse import ModuleManager from psyclone.parse.algorithm import parse from psyclone.psyGen import PSyFactory, InvokeSchedule from psyclone.psyir.symbols import (DataSymbol, REAL_TYPE, INTEGER_TYPE, - CHARACTER_TYPE, Symbol, SymbolError) + CHARACTER_TYPE, Symbol) from psyclone.tests.utilities import get_invoke, make_external_module from psyclone.transformations import (KernelImportsToArguments, TransformationError) @@ -150,7 +149,8 @@ def test_kernelimportstoargumentstrans(monkeypatch, fortran_writer): # Check the kernel code is generated as expected kernel_code = fortran_writer(kernel.get_callees()[0]) - assert "subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, rdt, magic)" in kernel_code + assert ("subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, " + "rdt, magic)" in kernel_code) assert "real(kind=go_wp), intent(in) :: rdt" in kernel_code assert "real(kind=go_wp), intent(inout) :: magic" in kernel_code @@ -165,10 +165,9 @@ def test_kernelimportstoargumentstrans(monkeypatch, fortran_writer): @pytest.mark.usefixtures("kernel_outputdir") -def test_kernelimportstoargumentstrans_constant(monkeypatch): +def test_kernelimportstoargumentstrans_constant(monkeypatch, fortran_writer): ''' Check the KernelImportsToArguments transformation when the import is also a constant value, in this case the argument should be read-only.''' - from psyclone.psyir.backend.fortran import FortranWriter from psyclone.psyir.nodes import Literal trans = KernelImportsToArguments() @@ -189,12 +188,12 @@ def create_data_symbol(arg): monkeypatch.setattr(DataSymbol, "resolve_type", create_data_symbol) monkeypatch.setattr(Symbol, "resolve_type", create_data_symbol) - # Test transforming a single kernel + # Test transforming a single kernel. We have to module-inline it first. + KernelModuleInlineTrans().apply(kernel) trans.apply(kernel) - fwriter = FortranWriter() kernels = kernel.get_callees() - kernel_code = fwriter(kernels[0]) + kernel_code = fortran_writer(kernels[0]) assert ("subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, rdt, " "magic)" in kernel_code) @@ -223,7 +222,8 @@ def create_data_symbol(arg): return symbol monkeypatch.setattr(Symbol, "resolve_type", create_data_symbol) - # Test transforming a single kernel + # Test transforming a single kernel - have to module-inline it first. + KernelModuleInlineTrans().apply(kernel) with pytest.raises(TypeError) as err: trans.apply(kernel) assert ("The imported variable 'rdt' could not be promoted to an argument " @@ -232,13 +232,10 @@ def create_data_symbol(arg): in str(err.value)) -@pytest.mark.usefixtures("kernel_outputdir") -def test_kernelimportstoarguments_multiple_kernels(monkeypatch): +def test_kernelimportstoarguments_multiple_kernels(): ''' Check the KernelImportsToArguments transformation with an invoke with three kernel calls, two of them duplicated and the third one sharing the same imported module''' - from psyclone.psyir.backend.fortran import FortranWriter - fwriter = FortranWriter() # Construct a testing InvokeSchedule _, invoke_info = parse(os.path. @@ -248,62 +245,43 @@ def test_kernelimportstoarguments_multiple_kernels(monkeypatch): psy = PSyFactory(API).create(invoke_info) invoke = psy.invokes.invoke_list[0] trans = KernelImportsToArguments() + mod_inline_trans = KernelModuleInlineTrans() # The kernels are checked before the psy.gen, so they don't include the # modified suffix. expected = [ ["subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, rdt, " "magic)", - "real, intent(inout) :: rdt"], + "real(kind=go_wp), intent(in) :: rdt"], ["subroutine kernel_with_use2_code(ji, jj, istep, ssha, tmask, cbfr," " rdt)", - "real, intent(inout) :: cbfr\n real, intent(inout) :: rdt"], + "real(kind=go_wp), intent(inout) :: cbfr\n real(kind=go_wp), " + "intent(in) :: rdt"], ["subroutine kernel_with_use_code(ji, jj, istep, ssha, tmask, rdt, " "magic)", - "real, intent(inout) :: rdt\n real, intent(inout) :: magic"]] + "real(kind=go_wp), intent(in) :: rdt\n real(kind=go_wp), " + "intent(inout) :: magic"]] - # Monkeypatch the resolve_type() methods to avoid searching and - # importing of module during this test. - def create_data_symbol(arg): - symbol = DataSymbol(arg.name, REAL_TYPE, - interface=arg.interface) - return symbol - monkeypatch.setattr(Symbol, "resolve_type", create_data_symbol) - monkeypatch.setattr(DataSymbol, "resolve_type", create_data_symbol) + # Ensure the ModuleManager can find the necessary files. + mod_man = ModuleManager.get() + mod_man.add_search_path(os.path.join(BASEPATH, "gocean1p0")) for num, kernel in enumerate(invoke.schedule.coded_kernels()): - kernels = kernel.get_callees() - kschedule = kernels[0] - + mod_inline_trans.apply(kernel) trans.apply(kernel) - # Check the kernel code is generated as expected - kernel_code = fwriter(kschedule) - for part in expected[num]: - assert part in kernel_code - generated_code = str(psy.gen) - # The following assert checks that imports from the same module are - # imported, since the kernels are marked as modified, new suffixes are - # given in order to differentiate each of them. - assert ("use kernel_with_use_1_mod, only : kernel_with_use_1_code\n" - in generated_code) - assert ("use kernel_with_use2_0_mod, only : kernel_with_use2_0_code\n" - in generated_code) - assert ("use kernel_with_use_0_mod, only : kernel_with_use_0_code\n" - in generated_code) - - # Check the kernel calls have the imported symbol passed as last argument - assert ("call kernel_with_use_0_code(i, j, oldu_fld, cu_fld%data, " - "cu_fld%grid%tmask, rdt, magic)" in generated_code) - assert ("call kernel_with_use_1_code(i, j, oldu_fld, cu_fld%data, " - "cu_fld%grid%tmask, rdt, magic)" in generated_code) - assert ("call kernel_with_use2_0_code(i, j, oldu_fld, cu_fld%data, " - "cu_fld%grid%tmask, cbfr, rdt)" in generated_code) + # Check the kernel code is generated as expected + for num in range(len(invoke.schedule.coded_kernels())): + for part in expected[num]: + assert part in generated_code, part + + # Kernels not imported anymore. + assert "use kernel_with_use_mod" not in generated_code + assert "use kernel_with_use2_mod" not in generated_code -@pytest.mark.usefixtures("kernel_outputdir") def test_kernelimportstoarguments_noimports(fortran_writer): ''' Check the KernelImportsToArguments transformation can be applied to a kernel that does not contain any import without any effect ''' @@ -316,8 +294,10 @@ def test_kernelimportstoarguments_noimports(fortran_writer): invoke = psy.invokes.invoke_list[0] kernel = invoke.schedule.coded_kernels()[0] - before_code = fortran_writer(psy.container) trans = KernelImportsToArguments() + mod_inline_trans = KernelModuleInlineTrans() + mod_inline_trans.apply(kernel) + before_code = fortran_writer(psy.container) trans.apply(kernel) after_code = fortran_writer(psy.container) @@ -340,6 +320,8 @@ def test_kernelimportstoargumentstrans_clash_symboltable(monkeypatch, end module model_mod""") trans = KernelImportsToArguments() + mod_inline_trans = KernelModuleInlineTrans() + # Construct a testing InvokeSchedule _, invoke = get_invoke("single_invoke_kern_with_use.f90", idx=0, api=API) kernel = invoke.schedule.coded_kernels()[0] @@ -348,6 +330,8 @@ def test_kernelimportstoargumentstrans_clash_symboltable(monkeypatch, kernel.ancestor(InvokeSchedule).symbol_table.add( DataSymbol("rdt", REAL_TYPE)) + mod_inline_trans.apply(kernel) + # Test transforming a single kernel with pytest.raises(KeyError) as err: trans.apply(kernel) diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index ba51d112df..5517eeef33 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -74,7 +74,7 @@ from psyclone.psyir.nodes.structure_reference import StructureReference from psyclone.psyir.symbols import ( ArgumentInterface, DataSymbol, INTEGER_TYPE, ScalarType, Symbol, - SymbolError, UnresolvedType) + UnresolvedType) from psyclone.psyir.transformations.loop_trans import LoopTrans from psyclone.psyir.transformations.omp_loop_trans import OMPLoopTrans from psyclone.psyir.transformations.parallel_loop_trans import ( @@ -2498,6 +2498,8 @@ def apply(self, node, options=None): kernel = kernels[0] symtab = kernel.symbol_table invoke_symtab = node.ancestor(InvokeSchedule).symbol_table + precision_sym_names = [sym.name.lower() for sym in + kernel.symbol_table.precision_datasymbols] count_imported_vars_removed = 0 # Transform each imported variable into an argument. @@ -2514,8 +2516,7 @@ def apply(self, node, options=None): # If we have a new symbol then we must update the symbol table if updated_sym is not imported_var: kernel.symbol_table.swap(imported_var, updated_sym) - - if updated_sym in kernel.symbol_table.precision_datasymbols: + if updated_sym.name.lower() in precision_sym_names: # Symbols specifying compile-time precision can't be passed # as arguments. continue From 564e46dc9d17c5ccf26bacbb322d68d019c5c715 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 26 Jan 2026 11:10:49 +0000 Subject: [PATCH 08/17] #1823 more test fixes [skip ci] --- .../gocean1p0_transformations_test.py | 9 +- src/psyclone/tests/gocean1p0_test.py | 44 ++-- src/psyclone/tests/lfric_multigrid_test.py | 8 +- .../kernel_transformation_test.py | 241 ++---------------- 4 files changed, 56 insertions(+), 246 deletions(-) diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py index 3877b72240..6d468d4ffc 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py @@ -48,7 +48,7 @@ from psyclone.gocean1p0 import GOKern from psyclone.parse import ModuleManager from psyclone.psyGen import Kern -from psyclone.psyir.nodes import Loop +from psyclone.psyir.nodes import Container, Loop from psyclone.psyir.transformations import ( LoopFuseTrans, LoopTrans, TransformationError, OMPParallelTrans) @@ -1392,8 +1392,12 @@ def test_acc_enter_directive_infrastructure_setup_error(): accdata.apply(schedule) # Remove the InvokeSchedule from its Container so that OpenACC will not - # find where to add the read_from_device function. + # find where to add the read_from_device function. However, we have to + # put the symbol representing the Kernel routine into the local table + # in order to get to that error. + sym = schedule.ancestor(Container).symbol_table.lookup("compute_cu_code") schedule.detach() + schedule.symbol_table.add(sym) # Generate the code with pytest.raises(GenerationError) as err: @@ -1512,6 +1516,7 @@ def test_accroutinetrans_with_kern(fortran_writer, monkeypatch): assert isinstance(kern, GOKern) rtrans = ACCRoutineTrans() assert rtrans.name == "ACCRoutineTrans" + KernelModuleInlineTrans().apply(kern) rtrans.apply(kern) # Check that there is a acc routine directive in the kernel schedules = kern.get_callees() diff --git a/src/psyclone/tests/gocean1p0_test.py b/src/psyclone/tests/gocean1p0_test.py index 6d615d2579..1c4f086570 100644 --- a/src/psyclone/tests/gocean1p0_test.py +++ b/src/psyclone/tests/gocean1p0_test.py @@ -82,11 +82,11 @@ def test_field(tmpdir, dist_mem): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use compute_cu_mod, only : compute_cu_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_compute_cu(cu_fld, p_fld, u_fld)\n" - " use compute_cu_mod, only : compute_cu_code\n" " type(r2d_field), intent(inout) :: cu_fld\n" " type(r2d_field), intent(inout) :: p_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" @@ -154,13 +154,13 @@ def test_two_kernels(tmpdir, dist_mem): "module psy_single_invoke_two_kernels\n" " use field_mod\n" " use kind_params_mod\n" + " use compute_cu_mod, only : compute_cu_code\n" + " use time_smooth_mod, only : time_smooth_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0(cu_fld, p_fld, u_fld, unew_fld, " "uold_fld)\n" - " use compute_cu_mod, only : compute_cu_code\n" - " use time_smooth_mod, only : time_smooth_code\n" " type(r2d_field), intent(inout) :: cu_fld\n" " type(r2d_field), intent(inout) :: p_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" @@ -210,11 +210,11 @@ def test_two_kernels_with_dependencies(tmpdir, dist_mem): "module psy_single_invoke_two_kernels\n" " use field_mod\n" " use kind_params_mod\n" + " use compute_cu_mod, only : compute_cu_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0(cu_fld, p_fld, u_fld)\n" - " use compute_cu_mod, only : compute_cu_code\n" " type(r2d_field), intent(inout) :: cu_fld\n" " type(r2d_field), intent(inout) :: p_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" @@ -268,11 +268,11 @@ def test_grid_property(tmpdir, dist_mem): "module psy_single_invoke_with_grid_props_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_requires_grid_props, only : next_sshu_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0(cu_fld, u_fld, du_fld, d_fld)\n" - " use kernel_requires_grid_props, only : next_sshu_code\n" " type(r2d_field), intent(inout) :: cu_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" " type(r2d_field), intent(inout) :: du_fld\n" @@ -325,11 +325,11 @@ def test_scalar_int_arg(tmpdir, dist_mem): "module psy_single_invoke_scalar_int_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_scalar_int, only : bc_ssh_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_bc_ssh(ncycle, ssh_fld)\n" - " use kernel_scalar_int, only : bc_ssh_code\n" " integer, intent(inout) :: ncycle\n" " type(r2d_field), intent(inout) :: ssh_fld\n" " integer :: j\n" @@ -369,11 +369,11 @@ def test_scalar_float_arg(tmpdir, dist_mem): "module psy_single_invoke_scalar_float_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_scalar_float, only : bc_ssh_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_bc_ssh(a_scalar, ssh_fld)\n" - " use kernel_scalar_float, only : bc_ssh_code\n" " real(kind=go_wp), intent(inout) :: a_scalar\n" " type(r2d_field), intent(inout) :: ssh_fld\n" " integer :: j\n" @@ -427,12 +427,12 @@ def test_scalar_float_arg_from_module(): "module psy_single_invoke_scalar_float_test\n" " use field_mod\n" " use kind_params_mod\n" + " use my_mod, only : a_scalar\n" + " use kernel_scalar_float, only : bc_ssh_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_bc_ssh(ssh_fld)\n" - " use my_mod, only : a_scalar\n" - " use kernel_scalar_float, only : bc_ssh_code\n" " type(r2d_field), intent(inout) :: ssh_fld\n" " integer :: j\n" " integer :: i\n" @@ -478,11 +478,11 @@ def test_ne_offset_cf_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_ne_offset_cf_mod, only : compute_vort_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_compute_vort(vort_fld, p_fld, u_fld, v_fld)\n" - " use kernel_ne_offset_cf_mod, only : compute_vort_code\n" " type(r2d_field), intent(inout) :: vort_fld\n" " type(r2d_field), intent(inout) :: p_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" @@ -529,11 +529,11 @@ def test_ne_offset_ct_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_ne_offset_ct_mod, only : compute_vort_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_compute_vort(p_fld, u_fld, v_fld)\n" - " use kernel_ne_offset_ct_mod, only : compute_vort_code\n" " type(r2d_field), intent(inout) :: p_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" " type(r2d_field), intent(inout) :: v_fld\n" @@ -579,11 +579,11 @@ def test_ne_offset_all_cu_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use boundary_conditions_ne_offset_mod, only : bc_solid_u_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_bc_solid_u(u_fld)\n" - " use boundary_conditions_ne_offset_mod, only : bc_solid_u_code\n" " type(r2d_field), intent(inout) :: u_fld\n" " integer :: j\n" " integer :: i\n" @@ -626,11 +626,11 @@ def test_ne_offset_all_cv_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use boundary_conditions_ne_offset_mod, only : bc_solid_v_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_bc_solid_v(v_fld)\n" - " use boundary_conditions_ne_offset_mod, only : bc_solid_v_code\n" " type(r2d_field), intent(inout) :: v_fld\n" " integer :: j\n" " integer :: i\n" @@ -673,11 +673,11 @@ def test_ne_offset_all_cf_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use boundary_conditions_ne_offset_mod, only : bc_solid_f_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_bc_solid_f(f_fld)\n" - " use boundary_conditions_ne_offset_mod, only : bc_solid_f_code\n" " type(r2d_field), intent(inout) :: f_fld\n" " integer :: j\n" " integer :: i\n" @@ -718,11 +718,11 @@ def test_sw_offset_cf_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_sw_offset_cf_mod, only : compute_z_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_compute_z(z_fld, p_fld, u_fld, v_fld)\n" - " use kernel_sw_offset_cf_mod, only : compute_z_code\n" " type(r2d_field), intent(inout) :: z_fld\n" " type(r2d_field), intent(inout) :: p_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" @@ -769,11 +769,11 @@ def test_sw_offset_all_cf_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_sw_offset_cf_mod, only : apply_bcs_f_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_apply_bcs_f(z_fld, p_fld, u_fld, v_fld)\n" - " use kernel_sw_offset_cf_mod, only : apply_bcs_f_code\n" " type(r2d_field), intent(inout) :: z_fld\n" " type(r2d_field), intent(inout) :: p_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" @@ -820,11 +820,11 @@ def test_sw_offset_ct_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_sw_offset_ct_mod, only : compute_h_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_compute_h(h_fld, p_fld, u_fld, v_fld)\n" - " use kernel_sw_offset_ct_mod, only : compute_h_code\n" " type(r2d_field), intent(inout) :: h_fld\n" " type(r2d_field), intent(inout) :: p_fld\n" " type(r2d_field), intent(inout) :: u_fld\n" @@ -872,11 +872,11 @@ def test_sw_offset_all_ct_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_sw_offset_ct_mod, only : apply_bcs_h_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_apply_bcs_h(hfld, pfld, ufld, vfld)\n" - " use kernel_sw_offset_ct_mod, only : apply_bcs_h_code\n" " type(r2d_field), intent(inout) :: hfld\n" " type(r2d_field), intent(inout) :: pfld\n" " type(r2d_field), intent(inout) :: ufld\n" @@ -924,11 +924,11 @@ def test_sw_offset_all_cu_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_sw_offset_cu_mod, only : apply_bcs_u_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_apply_bcs_u(ufld, vfld)\n" - " use kernel_sw_offset_cu_mod, only : apply_bcs_u_code\n" " type(r2d_field), intent(inout) :: ufld\n" " type(r2d_field), intent(inout) :: vfld\n" " integer :: j\n" @@ -973,11 +973,11 @@ def test_sw_offset_all_cv_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_sw_offset_cv_mod, only : apply_bcs_v_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_apply_bcs_v(vfld, ufld)\n" - " use kernel_sw_offset_cv_mod, only : apply_bcs_v_code\n" " type(r2d_field), intent(inout) :: vfld\n" " type(r2d_field), intent(inout) :: ufld\n" " integer :: j\n" @@ -1022,11 +1022,11 @@ def test_offset_any_all_cu_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_any_offset_cu_mod, only : compute_u_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_compute_u(ufld, vfld, hfld)\n" - " use kernel_any_offset_cu_mod, only : compute_u_code\n" " type(r2d_field), intent(inout) :: ufld\n" " type(r2d_field), intent(inout) :: vfld\n" " type(r2d_field), intent(inout) :: hfld\n" @@ -1073,11 +1073,11 @@ def test_offset_any_all_points(tmpdir): "module psy_single_invoke_test\n" " use field_mod\n" " use kind_params_mod\n" + " use kernel_field_copy_mod, only : field_copy_code\n" " implicit none\n" " public\n\n" " contains\n" " subroutine invoke_0_copy(voldfld, vfld)\n" - " use kernel_field_copy_mod, only : field_copy_code\n" " type(r2d_field), intent(inout) :: voldfld\n" " type(r2d_field), intent(inout) :: vfld\n" " integer :: j\n" diff --git a/src/psyclone/tests/lfric_multigrid_test.py b/src/psyclone/tests/lfric_multigrid_test.py index bcc712e3a3..2096e9fecd 100644 --- a/src/psyclone/tests/lfric_multigrid_test.py +++ b/src/psyclone/tests/lfric_multigrid_test.py @@ -286,8 +286,9 @@ def test_field_prolong(tmpdir, dist_mem): assert LFRicBuild(tmpdir).code_compiles(psy) + assert ("use prolong_test_kernel_mod, only : prolong_test_kernel_code\n" + " implicit none" in code) expected = ( - " use prolong_test_kernel_mod, only : prolong_test_kernel_code\n" " use mesh_mod, only : mesh_type\n" " use mesh_map_mod, only : mesh_map_type\n" " type(field_type), intent(in) :: field1\n" @@ -386,9 +387,10 @@ def test_field_restrict(tmpdir, dist_mem, monkeypatch, annexed): assert LFRicBuild(tmpdir).code_compiles(psy) + assert ("use restrict_test_kernel_mod, only : restrict_test_kernel_code\n" + " implicit none" in output) + defs = ( - " use restrict_test_kernel_mod, " - "only : restrict_test_kernel_code\n" " use mesh_mod, only : mesh_type\n" " use mesh_map_mod, only : mesh_map_type\n" " type(field_type), intent(in) :: field1\n" diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index fd4bfd1e0a..853818d38b 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -48,7 +48,8 @@ from psyclone.domain.lfric.lfric_builtins import LFRicBuiltIn from psyclone.generator import GenerationError from psyclone.psyGen import Kern -from psyclone.psyir.nodes import Routine, FileContainer, IntrinsicCall, Call +from psyclone.psyir.nodes import (Call, Container, Routine, FileContainer, + IntrinsicCall) from psyclone.psyir.symbols import DataSymbol, INTEGER_TYPE from psyclone.psyir.transformations import ( TransformationError, OMPDeclareTargetTrans) @@ -74,223 +75,27 @@ def teardown_function(): Config._instance = None -def test_new_kernel_file(kernel_outputdir, monkeypatch, fortran_reader): - ''' Check that we write out the transformed kernel to the CWD. ''' - # Ensure kernel-output directory is uninitialised - config = Config.get() - monkeypatch.setattr(config, "_kernel_naming", "multiple") - psy, invoke = get_invoke("nemolite2d_alg_mod.f90", api="gocean", idx=0) - sched = invoke.schedule - kern = sched.coded_kernels()[0] - rtrans = ACCRoutineTrans() - rtrans.apply(kern) - # Generate the code (this triggers the generation of a new kernel) - code = str(psy.gen).lower() - # Work out the value of the tag used to re-name the kernel - tag = re.search('use continuity(.+?)_mod', code).group(1) - assert f"use continuity{tag}_mod, only : continuity{tag}_code" in code - assert f"call continuity{tag}_code(" in code - # The kernel and module name should have gained the tag just identified - # and be written to the CWD - filename = os.path.join(str(kernel_outputdir), f"continuity{tag}_mod.f90") - assert os.path.isfile(filename) - # Parse the new kernel file - psyir = fortran_reader.psyir_from_file(filename) - # Check that the module has the right name - assert isinstance(psyir, FileContainer) - module = psyir.children[0] - assert module.name == f"continuity{tag}_mod" - - # Check that the subroutine has the right name - for sub in psyir.walk(Routine): - if sub.name == f"continuity{tag}_code": - break - else: - assert False, f"Failed to find subroutine named continuity{tag}_code" - - # If compilation fails this will raise an exception - GOceanBuild(kernel_outputdir).compile_file(filename) - - -def test_new_kernel_dir(kernel_outputdir): - ''' Check that we write out the transformed kernel to a specified - directory. ''' - psy, invoke = get_invoke("nemolite2d_alg_mod.f90", api="gocean", idx=0) - sched = invoke.schedule - kern = sched.coded_kernels()[0] - rtrans = ACCRoutineTrans() - rtrans.apply(kern) - # Generate the code (this triggers the generation of a new kernel) - _ = str(psy.gen) - file_list = os.listdir(str(kernel_outputdir)) - assert len(file_list) == 1 - assert file_list[0] == 'continuity_0_mod.f90' - - -def test_new_kern_no_clobber(kernel_outputdir, monkeypatch): - ''' Check that we create a new kernel with a new name when kernel-naming - is set to 'multiple' and we would otherwise get a name clash. ''' - # Ensure kernel-output directory is uninitialised - config = Config.get() - monkeypatch.setattr(config, "_kernel_naming", "multiple") - psy, invoke = get_invoke("1_single_invoke.f90", api="lfric", idx=0) - sched = invoke.schedule - kernels = sched.walk(Kern) - kern = kernels[0] - old_mod_name = kern.module_name[:].lower() - if old_mod_name.endswith("_mod"): - old_mod_name = old_mod_name[:-4] - # Create a file with the same name as we would otherwise generate - with open(os.path.join(str(kernel_outputdir), - old_mod_name+"_0_mod.f90"), - "w", encoding="utf-8") as ffile: - ffile.write("some code") - rtrans = ACCRoutineTrans() - rtrans.apply(kern) - # Generate the code (this triggers the generation of a new kernel) - _ = str(psy.gen).lower() - filename = os.path.join(str(kernel_outputdir), old_mod_name+"_1_mod.f90") - assert os.path.isfile(filename) - - -@pytest.mark.parametrize( - "mod_name,sub_name", - [("testkern_mod", "testkern"), - ("testkern", "testkern_code"), - ("testkern1_mod", "testkern2_code")]) -def test_kernel_module_name(kernel_outputdir, mod_name, sub_name, monkeypatch): - '''Check that there is no limitation on kernel and module names. In - particular check that the names do not have to conform to the - _mod, _code convention. - - ''' - # Argument kernel_outputdir is needed to capture the files created by - # the rename_and_write() call - # pylint: disable=unused-argument - _, invoke = get_invoke("1_single_invoke.f90", api="lfric", idx=0) - sched = invoke.schedule - kernels = sched.coded_kernels() - kern = kernels[0] - ktrans = LFRicKernelConstTrans() - ktrans.apply(kern, {"number_of_layers": 100}) - # Modify the kernel module and subroutine names. - monkeypatch.setattr(kern, "_module_name", mod_name) - monkeypatch.setattr(kern, "_name", sub_name) - # Generate the code - no exception should be raised when the names - # do not conform to the _mod, >name>_code convention. - kern.rename_and_write() - - -@pytest.mark.parametrize( - "mod_name,sub_name", - [("testkern_mod", "testkern_code"), - ("testkern_MOD", "testkern_CODE"), - ("TESTKERN_mod", "testkern_code"), - ("testkern_mod", "TESTKERN_code"), - ("TESTKERN_MoD", "TESTKERN_CoDe")]) -def test_kern_case_insensitive(mod_name, sub_name, kernel_outputdir, - monkeypatch): - '''Check that the test to see if a kernel conforms to the _mod, - _code convention is case insensitive. This check also tests that the - removal of _mod to create part of the output filename is case - insensitive. - - ''' - _, invoke = get_invoke("1_single_invoke.f90", api="lfric", idx=0) - sched = invoke.schedule - kernels = sched.walk(Kern) - kern = kernels[0] - ktrans = LFRicKernelConstTrans() - ktrans.apply(kern, {"number_of_layers": 100}) - monkeypatch.setattr(kern, "_module_name", mod_name) - monkeypatch.setattr(kern, "_name", sub_name) - # Generate the code - this should not raise an exception. - kern.rename_and_write() - filename = os.path.join(str(kernel_outputdir), mod_name[:8]+"_0_mod.f90") - assert os.path.isfile(filename) - - -def test_new_kern_single_error(kernel_outputdir, monkeypatch): - ''' Check that we do not overwrite an existing, different kernel if - there is a name clash and kernel-naming is 'single'. ''' - # Ensure kernel-output directory is uninitialised - config = Config.get() - monkeypatch.setattr(config, "_kernel_naming", "single") - _, invoke = get_invoke("1_single_invoke.f90", api="lfric", idx=0) - sched = invoke.schedule - kernels = sched.coded_kernels() - kern = kernels[0] - old_mod_name = kern.module_name[:].lower() - if old_mod_name.endswith("_mod"): - old_mod_name = old_mod_name[:-4] - # Create a file with the same name as we would otherwise generate - with open(os.path.join(str(kernel_outputdir), - old_mod_name+"_0_mod.f90"), - "w", encoding="utf-8") as ffile: - ffile.write("some code") - rtrans = ACCRoutineTrans() - rtrans.apply(kern) - # Generate the code - this should raise an error as we get a name - # clash and the content of the existing file is not the same as that - # which we would generate - with pytest.raises(GenerationError) as err: - kern.rename_and_write() - assert (f"transformed version of this Kernel 'testkern_0_mod.f90' already " - f"exists in the kernel-output directory ({kernel_outputdir}) " - f"but is not the same as the current, transformed kernel and the " - f"kernel-renaming scheme is set to 'single'" in str(err.value)) - - -def test_new_same_kern_single(kernel_outputdir, monkeypatch): - ''' Check that we do not overwrite an existing, identical kernel if - there is a name clash and kernel-naming is 'single'. ''' - # Ensure kernel-output directory is uninitialised - config = Config.get() - monkeypatch.setattr(config, "_kernel_naming", "single") - rtrans = ACCRoutineTrans() - _, invoke = get_invoke("4_multikernel_invokes.f90", api="lfric", - idx=0) - sched = invoke.schedule - # Apply the same transformation to both kernels. This should produce - # two, identical transformed kernels. - new_kernels = [] - for kern in sched.coded_kernels(): - rtrans.apply(kern) - new_kernels.append(kern) - - # Generate the code - we should end up with just one transformed kernel - new_kernels[0].rename_and_write() - new_kernels[1].rename_and_write() - assert new_kernels[1]._name == "testkern_0_code" - assert new_kernels[1].module_name == "testkern_0_mod" - out_files = os.listdir(str(kernel_outputdir)) - assert out_files == [new_kernels[1].module_name+".f90"] - - -def test_transform_kern_with_interface(kernel_outputdir): +def test_transform_kern_with_interface(tmp_path, fortran_writer): ''' Test that we can transform a polymorphic kernel - i.e. one where there is more than one subroutine implementation in order to support different precisions. ''' + mod_inline_trans = KernelModuleInlineTrans() rtrans = ACCRoutineTrans() psy, invoke = get_invoke("26.8_mixed_precision_args.f90", api="lfric", idx=0) sched = invoke.schedule kernels = sched.coded_kernels() + # Have to module-inline the kernel in order to transform it. + mod_inline_trans.apply(kernels[0]) # Have to use 'force' because the test kernel contains a WRITE which # becomes a CodeBlock. rtrans.apply(kernels[0], options={"force": True}) - kernels[0].rename_and_write() - out_files = os.listdir(str(kernel_outputdir)) - filename = os.path.join(str(kernel_outputdir), out_files[0]) - assert os.path.isfile(filename) - with open(filename, - "r", encoding="utf-8") as ffile: - contents = ffile.read() + contents = fortran_writer(sched.ancestor(Container)) # Check that the interface name has been updated. - assert "interface mixed_0_code" in contents + assert "interface mixed_code" in contents assert ("module procedure :: mixed_code_32, mixed_code_64" in contents) # Check that the subroutines themselves haven't been renamed. @@ -303,10 +108,11 @@ def test_transform_kern_with_interface(kernel_outputdir): assert ('''real*8, dimension(op_ncell_3d,ndf_w0,ndf_w0), intent(in) :: op !$acc routine seq''' in contents) - assert LFRicBuild(kernel_outputdir).code_compiles(psy) + assert LFRicBuild(tmp_path).code_compiles(psy) kernels = sched.coded_kernels() + mod_inline_trans.apply(kernels[1]) rtrans.apply(kernels[1], options={"force": True}) - assert LFRicBuild(kernel_outputdir).code_compiles(psy) + assert LFRicBuild(tmp_path).code_compiles(psy) # The following tests test the MarkRoutineForGPUMixin validation, for this @@ -476,9 +282,11 @@ def test_kernel_gpu_annotation_trans(rtrans, expected_directive, fortran_writer): ''' Check that the GPU annotation transformations insert the proper directive inside PSyKAl kernel code ''' + mod_inline_trans = KernelModuleInlineTrans() _, invoke = get_invoke("1_single_invoke.f90", api="lfric", idx=0) sched = invoke.schedule kern = sched.coded_kernels()[0] + mod_inline_trans.apply(kern) rtrans.apply(kern) # Check that the directive has been added to the kernel code @@ -521,30 +329,25 @@ def test_kernel_gpu_annotation_device_id(rtrans, fortran_reader): in str(err.value)) -def test_1kern_trans(kernel_outputdir): +def test_1kern_trans(tmp_path): ''' Check that we generate the correct code when an invoke contains the same kernel more than once but only one of them is transformed. ''' psy, invoke = get_invoke("4_multikernel_invokes.f90", api="lfric", idx=0) sched = invoke.schedule kernels = sched.coded_kernels() - # We will transform the second kernel but not the first kern = kernels[1] + # We have to module-inline the kernel before we can transform it and that + # will affect all calls to that kernel in the invoke. + KernelModuleInlineTrans().apply(kern) rtrans = ACCRoutineTrans() rtrans.apply(kern) - # Generate the code (this triggers the generation of a new kernel) + # Generate the code code = str(psy.gen).lower() - tag = re.search('use testkern(.+?)_mod', code).group(1) - # We should have a USE for the original kernel and a USE for the new one - assert f"use testkern{tag}_mod, only : testkern{tag}_code" in code - assert "use testkern_mod, only : testkern_code" in code - # Similarly, we should have calls to both the original and new kernels - assert "call testkern_code(" in code - assert f"call testkern{tag}_code(" in code - first = code.find("call testkern_code(") - second = code.find(f"call testkern{tag}_code(") - assert first < second - assert LFRicBuild(kernel_outputdir).code_compiles(psy) + assert 'use testkern_mod' not in code + assert code.count("call testkern_code(") == 2 + assert "private :: testkern_code" in code + assert LFRicBuild(tmp_path).code_compiles(psy) def test_2kern_trans(kernel_outputdir): From 134cd051ca4f5a70a080fee58e8d74b4f4da523a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 26 Jan 2026 11:11:45 +0000 Subject: [PATCH 09/17] #1823 fix lint [skip ci] --- .../tests/psyir/transformations/kernel_transformation_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py index 853818d38b..ba108fee99 100644 --- a/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py +++ b/src/psyclone/tests/psyir/transformations/kernel_transformation_test.py @@ -39,7 +39,6 @@ ''' Module containing tests for kernel transformations. ''' -import os import re import pytest @@ -54,7 +53,6 @@ from psyclone.psyir.transformations import ( TransformationError, OMPDeclareTargetTrans) from psyclone.transformations import ACCRoutineTrans, LFRicKernelConstTrans -from psyclone.tests.gocean_build import GOceanBuild from psyclone.tests.lfric_build import LFRicBuild from psyclone.tests.utilities import get_invoke From 06341f7813bf07796cb424b3ad05ab3ac97ab3fb Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 26 Jan 2026 19:46:32 +0000 Subject: [PATCH 10/17] #1823 WIP fixing tests [skip ci] --- ...teration_boundaries_inside_kernel_trans.py | 152 +++++++++++------- .../transformations/gocean_opencl_trans.py | 16 +- src/psyclone/gocean1p0.py | 4 +- ...ion_boundaries_inside_kernel_trans_test.py | 36 +++-- .../gocean_opencl_trans_test.py | 71 ++++++-- .../tests/domain/lfric/dofkern_test.py | 4 +- .../domain/lfric/lfric_field_codegen_test.py | 17 +- .../domain/lfric/lfric_scalar_codegen_test.py | 55 +++---- .../lfric_transformations_test.py | 25 ++- src/psyclone/tests/lfric_quadrature_test.py | 11 +- src/psyclone/transformations.py | 28 ++-- 11 files changed, 267 insertions(+), 152 deletions(-) diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index 69f7ca049f..cda3bacb41 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -35,16 +35,19 @@ '''This module contains the GOMoveIterationBoundariesInsideKernelTrans.''' +from psyclone.domain.common.transformations.kernel_transformation_mixin import\ + KernelTransformationMixin from psyclone.psyir.transformations import TransformationError from psyclone.psyGen import Transformation, InvokeSchedule from psyclone.gocean1p0 import GOKern -from psyclone.psyir.nodes import (BinaryOperation, Reference, Loop, +from psyclone.psyir.nodes import (BinaryOperation, Container, Reference, Loop, Assignment, IfBlock, Return) from psyclone.psyir.symbols import (INTEGER_TYPE, ArgumentInterface, DataSymbol) -class GOMoveIterationBoundariesInsideKernelTrans(Transformation): +class GOMoveIterationBoundariesInsideKernelTrans(Transformation, + KernelTransformationMixin): ''' Provides a transformation that moves iteration boundaries that are encoded in the Loops lower_bound() and upper_bound() methods to a mask inside the kernel with the boundaries passed as kernel arguments. @@ -109,6 +112,8 @@ def validate(self, node, options=None): f"can only be applied to 'GOKern' nodes, but found " f"'{type(node).__name__}'.") + self._check_kernel_is_local(node) + def apply(self, node, options=None): '''Apply this transformation to the supplied node. @@ -120,64 +125,24 @@ def apply(self, node, options=None): ''' self.validate(node, options) - # Get useful references - invoke_st = node.ancestor(InvokeSchedule).symbol_table - inner_loop = node.ancestor(Loop) - outer_loop = inner_loop.ancestor(Loop) - cursor = outer_loop.position - - # Make sure the boundary symbols in the PSylayer exist - inv_xstart = invoke_st.find_or_create_tag( - "xstart_" + node.name, root_name="xstart", symbol_type=DataSymbol, - datatype=INTEGER_TYPE) - inv_xstop = invoke_st.find_or_create_tag( - "xstop_" + node.name, root_name="xstop", symbol_type=DataSymbol, - datatype=INTEGER_TYPE) - inv_ystart = invoke_st.find_or_create_tag( - "ystart_" + node.name, root_name="ystart", symbol_type=DataSymbol, - datatype=INTEGER_TYPE) - inv_ystop = invoke_st.find_or_create_tag( - "ystop_" + node.name, root_name="ystop", symbol_type=DataSymbol, - datatype=INTEGER_TYPE) + invoke_sched = node.ancestor(InvokeSchedule) + self._boundary_values_declare_and_init(node) - # If the kernel acts on the whole iteration space, the boundary values - # are not needed. This also avoids adding duplicated arguments if this - # transformation is applied more than once to the same kernel. But the - # declaration and initialisation above still needs to exist because the - # boundary variables are expected to exist by the generation code. - if (inner_loop.field_space == "go_every" and - outer_loop.field_space == "go_every" and - inner_loop.iteration_space == "go_all_pts" and - outer_loop.iteration_space == "go_all_pts"): - return node.root, None - - # Initialise the boundary values provided by the Loop construct - assign1 = Assignment.create(Reference(inv_xstart), - inner_loop.start_expr.copy()) - outer_loop.parent.children.insert(cursor, assign1) - cursor = cursor + 1 - assign2 = Assignment.create(Reference(inv_xstop), - inner_loop.stop_expr.copy()) - outer_loop.parent.children.insert(cursor, assign2) - cursor = cursor + 1 - assign3 = Assignment.create(Reference(inv_ystart), - outer_loop.start_expr.copy()) - outer_loop.parent.children.insert(cursor, assign3) - cursor = cursor + 1 - assign4 = Assignment.create(Reference(inv_ystop), - outer_loop.stop_expr.copy()) - outer_loop.parent.children.insert(cursor, assign4) + # Check that this transformation hasn't already been applied to + # the associated kernel implementation (which has been module inlined). + ksched = node.get_callees()[0] + all_tags = ksched.symbol_table.get_tags(scope_limit=ksched) + if "xstart_arg" in all_tags: + return - # Update Kernel Call argument list - for symbol in [inv_xstart, inv_xstop, inv_ystart, inv_ystop]: - node.arguments.append(symbol.name, "go_i_scalar") - - # Now that the boundaries are inside the kernel, the looping should go - # through all the field points - inner_loop.field_space = "go_every" - outer_loop.field_space = "go_every" - inner_loop.iteration_space = "go_all_pts" - outer_loop.iteration_space = "go_all_pts" + # Update Kernel Call argument list. We have to do this for *every* + # matching Kernel since they all call the same, module-inlined, + # implementation. + for kern in node.ancestor(Container).walk(GOKern): + if kern.name == node.name: + bvalues = self._boundary_values_declare_and_init(kern) + for symbol in bvalues: + kern.arguments.append(symbol.name, "go_i_scalar") # Update Kernel implementation(s). for kschedule in node.get_callees(): @@ -189,16 +154,20 @@ def apply(self, node, options=None): # Create new symbols and insert them as kernel arguments at the # end of the kernel argument list xstart_symbol = kernel_st.new_symbol( - "xstart", symbol_type=DataSymbol, datatype=INTEGER_TYPE, + "xstart", tag="xstart_arg", + symbol_type=DataSymbol, datatype=INTEGER_TYPE, interface=ArgumentInterface(ArgumentInterface.Access.READ)) xstop_symbol = kernel_st.new_symbol( - "xstop", symbol_type=DataSymbol, datatype=INTEGER_TYPE, + "xstop", tag="xstop_arg", + symbol_type=DataSymbol, datatype=INTEGER_TYPE, interface=ArgumentInterface(ArgumentInterface.Access.READ)) ystart_symbol = kernel_st.new_symbol( - "ystart", symbol_type=DataSymbol, datatype=INTEGER_TYPE, + "ystart", tag="ystart_arg", + symbol_type=DataSymbol, datatype=INTEGER_TYPE, interface=ArgumentInterface(ArgumentInterface.Access.READ)) ystop_symbol = kernel_st.new_symbol( - "ystop", symbol_type=DataSymbol, datatype=INTEGER_TYPE, + "ystop", tag="ystop_arg", + symbol_type=DataSymbol, datatype=INTEGER_TYPE, interface=ArgumentInterface(ArgumentInterface.Access.READ)) kernel_st.specify_argument_list( iteration_indices + data_arguments + @@ -238,6 +207,65 @@ def apply(self, node, options=None): if_statement = IfBlock.create(condition, [Return()]) kschedule.children.insert(0, if_statement) + def _boundary_values_declare_and_init(self, node): + ''' + ''' + # Get useful references + invoke_st = node.ancestor(InvokeSchedule).symbol_table + inner_loop = node.ancestor(Loop) + outer_loop = inner_loop.ancestor(Loop) + cursor = outer_loop.position + + # Make sure the boundary symbols in the PSylayer exist + inv_xstart = invoke_st.find_or_create_tag( + "xstart_" + node.name, root_name="xstart", symbol_type=DataSymbol, + datatype=INTEGER_TYPE) + inv_xstop = invoke_st.find_or_create_tag( + "xstop_" + node.name, root_name="xstop", symbol_type=DataSymbol, + datatype=INTEGER_TYPE) + inv_ystart = invoke_st.find_or_create_tag( + "ystart_" + node.name, root_name="ystart", symbol_type=DataSymbol, + datatype=INTEGER_TYPE) + inv_ystop = invoke_st.find_or_create_tag( + "ystop_" + node.name, root_name="ystop", symbol_type=DataSymbol, + datatype=INTEGER_TYPE) + + # If the kernel acts on the whole iteration space, the boundary values + # are not needed. This also avoids adding duplicated arguments if this + # transformation is applied more than once to the same kernel. But the + # declaration and initialisation above still needs to exist because the + # boundary variables are expected to exist by the generation code. + if (inner_loop.field_space == "go_every" and + outer_loop.field_space == "go_every" and + inner_loop.iteration_space == "go_all_pts" and + outer_loop.iteration_space == "go_all_pts"): + return (inv_xstart, inv_xstop, inv_ystart, inv_ystop) + + # Initialise the boundary values provided by the Loop construct + assign1 = Assignment.create(Reference(inv_xstart), + inner_loop.start_expr.copy()) + outer_loop.parent.children.insert(cursor, assign1) + cursor = cursor + 1 + assign2 = Assignment.create(Reference(inv_xstop), + inner_loop.stop_expr.copy()) + outer_loop.parent.children.insert(cursor, assign2) + cursor = cursor + 1 + assign3 = Assignment.create(Reference(inv_ystart), + outer_loop.start_expr.copy()) + outer_loop.parent.children.insert(cursor, assign3) + cursor = cursor + 1 + assign4 = Assignment.create(Reference(inv_ystop), + outer_loop.stop_expr.copy()) + outer_loop.parent.children.insert(cursor, assign4) + + # Now that the boundaries are inside the kernel, the looping should go + # through all the field points + inner_loop.field_space = "go_every" + outer_loop.field_space = "go_every" + inner_loop.iteration_space = "go_all_pts" + outer_loop.iteration_space = "go_all_pts" + + return (inv_xstart, inv_xstop, inv_ystart, inv_ystop) # For Sphinx AutoAPI documentation generation __all__ = ['GOMoveIterationBoundariesInsideKernelTrans'] diff --git a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py index c7353e2272..1618c22c70 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_opencl_trans.py @@ -198,15 +198,17 @@ def validate(self, node, options=None): # any form of global data (that is not a routine argument or just # type information). for kern in node.kernels(): - KernelModuleInlineTrans().validate(kern) - + if not kern.module_inline: + KernelModuleInlineTrans().validate(kern) for ksched in kern.get_callees(): - global_variables = set(ksched.symbol_table.imported_symbols) - prec_symbols = set(ksched.symbol_table.precision_datasymbols) - if global_variables.difference(prec_symbols): - names = sorted([sym.name for sym in - global_variables.difference(prec_symbols)]) + global_variables = set(sym.name for sym in + ksched.symbol_table.imported_symbols) + prec_sym_names = set(sym.name for sym in + ksched.symbol_table.precision_datasymbols) + non_prec_vars = global_variables.difference(prec_sym_names) + if non_prec_vars: + names = sorted(non_prec_vars) raise TransformationError( f"The Symbol Table for kernel '{kern.name}' contains " f"the following symbols with 'global' scope: {names}. " diff --git a/src/psyclone/gocean1p0.py b/src/psyclone/gocean1p0.py index e5b3459d0d..73d8babff7 100644 --- a/src/psyclone/gocean1p0.py +++ b/src/psyclone/gocean1p0.py @@ -1329,7 +1329,9 @@ def psyir_expression(self): pass # Otherwise it's some form of Reference - symbol = self._call.scope.symbol_table.lookup(self.name) + symbol = self._call.scope.symbol_table.lookup(self.name, otherwise=None) + if not symbol: + import pdb; pdb.set_trace() # Gocean field arguments are StructureReferences to the %data attribute if self.argument_type == "field": diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py index 76940da01c..563e61eee7 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans_test.py @@ -39,14 +39,16 @@ ''' import pytest -from psyclone.tests.utilities import get_invoke + +from psyclone.domain.common.transformations import KernelModuleInlineTrans +from psyclone.gocean1p0 import GOLoop from psyclone.domain.gocean.transformations import ( GOMoveIterationBoundariesInsideKernelTrans) from psyclone.psyir.nodes import ( Assignment, Container, IfBlock, Return) -from psyclone.psyir.symbols import ArgumentInterface -from psyclone.gocean1p0 import GOLoop +from psyclone.psyir.symbols import ArgumentInterface, DataSymbol, INTEGER_TYPE from psyclone.psyir.transformations import TransformationError +from psyclone.tests.utilities import get_invoke API = "gocean" @@ -80,12 +82,15 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): num_args = len(kernel.arguments.args) # Add some name conflicting symbols in the Invoke and the Kernel - kernel.ancestor(Container).symbol_table.new_symbol("xstop") + kernel.ancestor(Container).symbol_table.new_symbol( + "xstop", symbol_type=DataSymbol, datatype=INTEGER_TYPE) routines = kernel.get_callees() ksched = routines[0] - ksched.symbol_table.new_symbol("ystart") + ksched.symbol_table.new_symbol( + "ystart", symbol_type=DataSymbol, datatype=INTEGER_TYPE) # Apply the transformation + KernelModuleInlineTrans().apply(kernel) trans = GOMoveIterationBoundariesInsideKernelTrans() trans.apply(kernel) @@ -132,7 +137,7 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): "Reference[name:'xstart']\n" "BinaryOperation[operator:'GT']\n" "Reference[name:'i']\n" - "Reference[name:'xstop']\n" + "Reference[name:'xstop_1']\n" "BinaryOperation[operator:'OR']\n" "BinaryOperation[operator:'LT']\n" "Reference[name:'j']\n" @@ -145,7 +150,7 @@ def test_go_move_iteration_boundaries_inside_kernel_trans(): # - It has the boundary symbol as kernel arguments assert isinstance(kschedule.symbol_table.lookup("xstart").interface, ArgumentInterface) - assert isinstance(kschedule.symbol_table.lookup("xstop").interface, + assert isinstance(kschedule.symbol_table.lookup("xstop_1").interface, ArgumentInterface) assert isinstance(kschedule.symbol_table.lookup("ystart_1").interface, ArgumentInterface) @@ -162,19 +167,24 @@ def test_go_move_iteration_boundaries_inside_kernel_two_kernels_apply_twice( postfixed with a number) and that kernels don't duplicate boundary arguments themself when applying the transformation twice. ''' - psy, _ = get_invoke("single_invoke_two_kernels.f90", API, idx=0, - dist_mem=False) - sched = psy.invokes.invoke_list[0].schedule + psy, invoke = get_invoke("single_invoke_two_kernels.f90", API, idx=0, + dist_mem=False) + sched = invoke.schedule # Apply the transformation twice + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) trans.apply(kernel) + output = fortran_writer(sched) + + assert "use compute_cu_mod" not in output + assert "use time_smooth_mod" not in output + expected = '''subroutine invoke_0(cu_fld, p_fld, u_fld, unew_fld, uold_fld) - use compute_cu_mod, only : compute_cu_code - use time_smooth_mod, only : time_smooth_code type(r2d_field), intent(inout) :: cu_fld type(r2d_field), intent(inout) :: p_fld type(r2d_field), intent(inout) :: u_fld @@ -215,4 +225,4 @@ def test_go_move_iteration_boundaries_inside_kernel_two_kernels_apply_twice( end subroutine invoke_0 ''' - assert fortran_writer(sched) == expected + assert expected in output diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean_opencl_trans_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean_opencl_trans_test.py index f89f2e5782..acaae7c722 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean_opencl_trans_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean_opencl_trans_test.py @@ -42,6 +42,7 @@ import pytest from psyclone.configuration import Config +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.gocean.transformations import ( GOMoveIterationBoundariesInsideKernelTrans, GOOpenCLTrans) from psyclone.errors import GenerationError @@ -73,6 +74,12 @@ def setup(): Config._instance = None +@pytest.fixture(name="mod_inline_trans") +def make_mod_inline_trans() -> KernelModuleInlineTrans: + '''Creates and returns a KernelModuleInlineTrans transformation.''' + return KernelModuleInlineTrans() + + # PSyclone API under test API = "gocean" @@ -130,9 +137,12 @@ def test_ocl_apply(kernel_outputdir): "one_invoke.f90", API, idx=0, dist_mem=False) schedule = invoke.schedule # Currently, moving the boundaries inside the kernel is a prerequisite - # for the GOcean gen_ocl() code generation. + # for the GOcean gen_ocl() code generation and module-inlining the kernel + # is a prerequisite for that. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in schedule.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) ocl = GOOpenCLTrans() @@ -167,8 +177,10 @@ def test_invoke_use_stmts_and_decls(kernel_outputdir, monkeypatch, debug_mode, # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -214,12 +226,14 @@ def test_invoke_use_stmts_and_decls(kernel_outputdir, monkeypatch, debug_mode, def test_invoke_opencl_initialisation(kernel_outputdir, fortran_writer): ''' Test that generating code for OpenCL results in the correct OpenCL first time initialisation code ''' - psy, _ = get_invoke("single_invoke.f90", API, idx=0) - sched = psy.invokes.invoke_list[0].schedule + psy, invoke = get_invoke("single_invoke.f90", API, idx=0) + sched = invoke.schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -291,12 +305,14 @@ def test_invoke_opencl_initialisation_grid(): ''' Test that generating OpenCL generation code when there are grid property accesses generated the proper grid on device initialisation code ''' - psy, _ = get_invoke("driver_test.f90", API, idx=0) - sched = psy.invokes.invoke_list[0].schedule + psy, invoke = get_invoke("driver_test.f90", API, idx=0) + sched = invoke.schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -420,8 +436,10 @@ def test_opencl_routines_initialisation(kernel_outputdir): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -559,8 +577,10 @@ def test_psy_init_defaults(kernel_outputdir): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -585,7 +605,7 @@ def test_psy_init_defaults(kernel_outputdir): assert GOceanOpenCLBuild(kernel_outputdir).code_compiles(psy) -def test_psy_init_multiple_kernels(kernel_outputdir): +def test_psy_init_multiple_kernels(kernel_outputdir, mod_inline_trans): ''' Check that we create a psy_init() routine that sets-up the kernel_names correctly when there are multiple kernels, some of them repeated. ''' @@ -594,10 +614,12 @@ def test_psy_init_multiple_kernels(kernel_outputdir): API, idx=0, dist_mem=True) sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel and removing - # kernel imports are prerequisites for this test. + # kernel imports are prerequisites for this test. Module-inlining + # the kernel is a prerequisite for both of these. trans1 = GOMoveIterationBoundariesInsideKernelTrans() trans2 = KernelImportsToArguments() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans1.apply(kernel) trans2.apply(kernel) @@ -630,8 +652,10 @@ def test_psy_init_multiple_devices_per_node(kernel_outputdir, monkeypatch): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) # Test with a different configuration value for OCL_DEVICES_PER_NODE @@ -671,8 +695,10 @@ def test_psy_init_with_options(kernel_outputdir): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) # Use non-default kernel and transformation options @@ -696,8 +722,10 @@ def test_invoke_opencl_kernel_call(kernel_outputdir, monkeypatch, debug_mode): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -777,8 +805,10 @@ def test_opencl_kernel_boundaries_validation(): "GOOpenCLTrans." in str(err.value)) # After move the boundaries the OpenCL transformation should pass + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans.apply(sched) @@ -791,8 +821,10 @@ def test_opencl_options_validation(): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -852,10 +884,13 @@ def test_opencl_multi_invoke_options_validation(option_to_check): invoke2_schedule = psy.invokes.invoke_list[1].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in invoke1_schedule.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) for kernel in invoke2_schedule.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -878,8 +913,10 @@ def test_opencl_options_effects(): # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -899,6 +936,7 @@ def test_opencl_options_effects(): sched = psy.invokes.invoke_list[0].schedule trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) # Change kernel local_size to 4 sched.coded_kernels()[0].set_opencl_options({'local_size': 4}) @@ -912,6 +950,7 @@ def test_opencl_options_effects(): sched = psy.invokes.invoke_list[0].schedule trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) # Change kernel queue number to 2 (the barrier should then also go up to 2) sched.coded_kernels()[0].set_opencl_options({'queue_number': 2}) @@ -930,6 +969,7 @@ def test_opencl_options_effects(): sched = psy.invokes.invoke_list[0].schedule trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -950,6 +990,7 @@ def test_multiple_command_queues(dist_mem): dist_mem=dist_mem) sched = psy.invokes.invoke_list[0].schedule + mod_inline_trans = KernelModuleInlineTrans() # Set the boundaries inside the kernel trans = GOMoveIterationBoundariesInsideKernelTrans() @@ -958,6 +999,7 @@ def test_multiple_command_queues(dist_mem): # OCL_MANAGEMENT_QUEUE used by the haloexchange data transfer which will # use queue 1, therefore barriers will always be needed in this example. for idx, kernel in enumerate(sched.coded_kernels()): + mod_inline_trans.apply(kernel) trans.apply(kernel) kernel.set_opencl_options({'queue_number': idx+2}) @@ -992,8 +1034,10 @@ def test_set_kern_args(kernel_outputdir): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -1052,20 +1096,22 @@ def test_set_kern_args(kernel_outputdir): assert GOceanOpenCLBuild(kernel_outputdir).code_compiles(psy) -@pytest.mark.usefixtures("kernel_outputdir") -def test_set_kern_args_real_grid_property(): +def test_set_kern_args_real_grid_property(tmp_path): ''' Check that we generate correct code to set a real scalar grid property. ''' psy, _ = get_invoke("driver_test.f90", API, idx=0) sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. + mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() otrans.apply(sched) + generated_code = str(psy.gen) expected = '''\ subroutine compute_kernel_code_set_args(kernel_obj, out_fld, in_out_fld, \ @@ -1087,9 +1133,10 @@ def test_set_kern_args_real_grid_property(): assert expected in generated_code # TODO 284: Currently this example cannot be compiled because it needs to # import a module which won't be found on kernel_outputdir + assert GOceanOpenCLBuild(tmp_path).code_compiles(psy) -def test_set_kern_float_arg(kernel_outputdir): +def test_set_kern_float_arg(kernel_outputdir, mod_inline_trans): ''' Check that we generate correct code to set a real, scalar kernel argument. ''' psy, _ = get_invoke("single_invoke_scalar_float_arg.f90", API, idx=0) @@ -1098,6 +1145,7 @@ def test_set_kern_float_arg(kernel_outputdir): # for the GOcean gen_ocl() code generation. trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) otrans = GOOpenCLTrans() @@ -1193,7 +1241,7 @@ def test_opencl_kernel_missing_boundary_symbol(monkeypatch): "before attempting the OpenCL code generation." in str(err.value)) -def test_opencl_kernel_output_file(kernel_outputdir): +def test_opencl_kernel_output_file(kernel_outputdir, mod_inline_trans): '''Check that a new OpenCL file named opencl_kernels_{suffix}.cl is generated. ''' @@ -1203,6 +1251,7 @@ def test_opencl_kernel_output_file(kernel_outputdir): # for the GOcean gen_ocl() code generation. trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): + mod_inline_trans.apply(kernel) trans.apply(kernel) # Create a opencl_kernels_0.cl so another name is needed for the new file diff --git a/src/psyclone/tests/domain/lfric/dofkern_test.py b/src/psyclone/tests/domain/lfric/dofkern_test.py index af79b53639..04a720fef2 100644 --- a/src/psyclone/tests/domain/lfric/dofkern_test.py +++ b/src/psyclone/tests/domain/lfric/dofkern_test.py @@ -257,8 +257,8 @@ def test_multi_invoke_cell_dof_builtin(tmpdir, monkeypatch, annexed, dist_mem): # generated # Use statements - assert " use testkern_mod, only : testkern_code\n" in code - assert " use testkern_dofs_mod, only : testkern_dofs_code\n" in code + assert " use testkern_mod, only : testkern_code\n" in code + assert " use testkern_dofs_mod, only : testkern_dofs_code\n" in code if dist_mem: # Check mesh_mod is added to use statements assert " use mesh_mod, only : mesh_type\n" in code diff --git a/src/psyclone/tests/domain/lfric/lfric_field_codegen_test.py b/src/psyclone/tests/domain/lfric/lfric_field_codegen_test.py index 5aa0ddc261..5688e03b05 100644 --- a/src/psyclone/tests/domain/lfric/lfric_field_codegen_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_field_codegen_test.py @@ -46,6 +46,7 @@ from psyclone.parse.algorithm import parse from psyclone.psyGen import PSyFactory from psyclone.tests.lfric_build import LFRicBuild +from psyclone.tests.utilities import get_invoke # Constants @@ -70,12 +71,12 @@ def test_field(tmpdir): "module single_invoke_psy\n" " use constants_mod\n" " use field_mod, only : field_proxy_type, field_type\n" + " use testkern_mod, only : testkern_code\n" " implicit none\n" " public\n" "\n" " contains\n" " subroutine invoke_0_testkern_type(a, f1, f2, m1, m2)\n" - " use testkern_mod, only : testkern_code\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1\n" " type(field_type), intent(in) :: f2\n" @@ -311,13 +312,13 @@ def test_field_fs(tmpdir): module single_invoke_fs_psy use constants_mod use field_mod, only : field_proxy_type, field_type + use testkern_fs_mod, only : testkern_fs_code implicit none public contains subroutine invoke_0_testkern_fs_type(f1, f2, m1, m2, f3, f4, m3, m4, f5, \ f6, m5, m6, m7) - use testkern_fs_mod, only : testkern_fs_code use mesh_mod, only : mesh_type type(field_type), intent(in) :: f1 type(field_type), intent(in) :: f2 @@ -644,13 +645,13 @@ def test_int_field_fs(tmpdir): assert """module single_invoke_fs_int_field_psy use constants_mod use integer_field_mod, only : integer_field_proxy_type, integer_field_type + use testkern_fs_int_field_mod, only : testkern_fs_int_field_code implicit none public contains subroutine invoke_0_testkern_fs_int_field_type(f1, f2, m1, m2, f3, f4, m3, \ m4, f5, f6, m5, m6, f7, f8, m7) - use testkern_fs_int_field_mod, only : testkern_fs_int_field_code use mesh_mod, only : mesh_type type(integer_field_type), intent(in) :: f1 type(integer_field_type), intent(in) :: f2 @@ -1039,12 +1040,8 @@ def test_int_real_field_fs(dist_mem, tmpdir): spaces produces correct code. ''' - _, invoke_info = parse( - os.path.join(BASE_PATH, - "4.14_multikernel_invokes_real_int_field_fs.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=dist_mem).create(invoke_info) - + psy, _ = get_invoke("4.14_multikernel_invokes_real_int_field_fs.f90", + api=TEST_API, dist_mem=dist_mem, idx=0) generated_code = str(psy.gen) output = ( @@ -1052,7 +1049,9 @@ def test_int_real_field_fs(dist_mem, tmpdir): " use constants_mod\n" " use integer_field_mod, only : integer_field_proxy_type, " "integer_field_type\n" + " use testkern_fs_int_field_mod, only : testkern_fs_int_field_code\n" " use field_mod, only : field_proxy_type, field_type\n" + " use testkern_fs_mod, only : testkern_fs_code\n" " implicit none\n" " public\n\n" " contains\n" diff --git a/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py b/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py index a46cea5f8d..f97cbf880d 100644 --- a/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py @@ -45,6 +45,7 @@ from psyclone.parse.algorithm import parse from psyclone.psyGen import PSyFactory from psyclone.tests.lfric_build import LFRicBuild +from psyclone.tests.utilities import get_invoke # Constants BASE_PATH = os.path.join( @@ -59,15 +60,14 @@ def test_real_scalar(tmpdir): real scalar argument (plus fields). ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1_single_invoke.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) + psy, _ = get_invoke("1_single_invoke.f90", api=TEST_API, idx=0, + dist_mem=True) generated_code = str(psy.gen) + assert "use testkern_mod, only : testkern_code\n" in generated_code + expected = ( " subroutine invoke_0_testkern_type(a, f1, f2, m1, m2)\n" - " use testkern_mod, only : testkern_code\n" " use mesh_mod, only : mesh_type\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1\n" @@ -162,18 +162,16 @@ def test_int_scalar(tmpdir): integer scalar argument (plus fields). ''' - _, invoke_info = parse( - os.path.join(BASE_PATH, - "1.6.1_single_invoke_1_int_scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) + psy, _ = get_invoke("1.6.1_single_invoke_1_int_scalar.f90", dist_mem=True, + api=TEST_API, idx=0) generated_code = str(psy.gen) + assert ("use testkern_one_int_scalar_mod, only : " + "testkern_one_int_scalar_code\n" in generated_code) + expected = ( " subroutine invoke_0_testkern_one_int_scalar_type" "(f1, iflag, f2, m1, m2)\n" - " use testkern_one_int_scalar_mod, only : " - "testkern_one_int_scalar_code\n" " use mesh_mod, only : mesh_type\n" " type(field_type), intent(in) :: f1\n" " integer(kind=i_def), intent(in) :: iflag\n" @@ -269,18 +267,16 @@ def test_two_real_scalars(tmpdir): scalar arguments. ''' - _, invoke_info = parse( - os.path.join(BASE_PATH, - "1.9_single_invoke_2_real_scalars.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) + psy, _ = get_invoke("1.9_single_invoke_2_real_scalars.f90", api=TEST_API, + dist_mem=True, idx=0) generated_code = str(psy.gen) + assert ("use testkern_two_real_scalars_mod, only : " + "testkern_two_real_scalars_code\n" in generated_code) + expected = ( " subroutine invoke_0_testkern_two_real_scalars_type(a, f1, f2, " "m1, m2, b)\n" - " use testkern_two_real_scalars_mod, only : " - "testkern_two_real_scalars_code\n" " use mesh_mod, only : mesh_type\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1\n" @@ -377,16 +373,15 @@ def test_two_int_scalars(tmpdir): scalar arguments. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.6_single_invoke_2_int_scalars.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) + psy, _ = get_invoke("1.6_single_invoke_2_int_scalars.f90", api=TEST_API, + dist_mem=True, idx=0) generated_code = str(psy.gen) + assert ("use testkern_two_int_scalars_mod, only : " + "testkern_two_int_scalars_code\n" in generated_code) + expected = ( " subroutine invoke_0(iflag, f1, f2, m1, m2, istep)\n" - " use testkern_two_int_scalars_mod, only : " - "testkern_two_int_scalars_code\n" " use mesh_mod, only : mesh_type\n" " integer(kind=i_def), intent(in) :: iflag\n" " type(field_type), intent(in) :: f1\n" @@ -495,24 +490,22 @@ def test_three_scalars(tmpdir): types of valid scalar argument: 'real', 'integer' and 'logical'. ''' - _, invoke_info = parse(os.path.join(BASE_PATH, - "1.7_single_invoke_3scalar.f90"), - api=TEST_API) - psy = PSyFactory(TEST_API, distributed_memory=True).create(invoke_info) + psy, _ = get_invoke("1.7_single_invoke_3scalar.f90", api=TEST_API, + dist_mem=True, idx=0) generated_code = str(psy.gen) expected = ( "module single_invoke_psy\n" " use constants_mod\n" " use field_mod, only : field_proxy_type, field_type\n" + " use testkern_three_scalars_mod, only : " + "testkern_three_scalars_code\n" " implicit none\n" " public\n" "\n" " contains\n" " subroutine invoke_0_testkern_three_scalars_type(a, f1, f2, m1, " "m2, lswitch, istep)\n" - " use testkern_three_scalars_mod, only : " - "testkern_three_scalars_code\n" " use mesh_mod, only : mesh_type\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1\n" diff --git a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py index 8305e42ade..4601e3c4fa 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -46,6 +46,7 @@ from psyclone.configuration import Config from psyclone.core import AccessType, Signature +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.lfric.lfric_builtins import LFRicXInnerproductYKern from psyclone.domain.lfric.transformations import LFRicLoopFuseTrans from psyclone.domain.lfric import LFRicLoop @@ -7345,19 +7346,23 @@ def test_kern_const_apply(capsys, monkeypatch): " Modified nqp_h, arg position 21, value 3.\n" " Modified nqp_v, arg position 22, value 3.\n") + mod_inline_trans = KernelModuleInlineTrans() # element_order_ only + mod_inline_trans.apply(kernel) kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0}) result, _ = capsys.readouterr() assert result == element_order_expected # nlayers only kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") + mod_inline_trans.apply(kernel) kctrans.apply(kernel, {"number_of_layers": 20}) result, _ = capsys.readouterr() assert result == number_of_layers_expected # element_order_ and quadrature kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") + mod_inline_trans.apply(kernel) kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0, "quadrature": True}) result, _ = capsys.readouterr() @@ -7365,6 +7370,7 @@ def test_kern_const_apply(capsys, monkeypatch): # element_order_ and nlayers kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") + mod_inline_trans.apply(kernel) kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0, "number_of_layers": 20}) result, _ = capsys.readouterr() @@ -7372,6 +7378,7 @@ def test_kern_const_apply(capsys, monkeypatch): # element_order_, nlayers and quadrature kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") + mod_inline_trans.apply(kernel) kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0, "number_of_layers": 20, "quadrature": True}) result, _ = capsys.readouterr() @@ -7398,7 +7405,7 @@ def test_kern_const_anyspace_anydspace_apply(capsys): kernel = create_kernel("1.5.3_single_invoke_write_any_anyd_space.f90") kctrans = LFRicKernelConstTrans() - + KernelModuleInlineTrans().apply(kernel) kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0}) result, _ = capsys.readouterr() assert result == ( @@ -7423,7 +7430,7 @@ def test_kern_const_anyw2_apply(capsys): kernel = create_kernel("21.1_single_invoke_multi_anyw2.f90") kctrans = LFRicKernelConstTrans() - + KernelModuleInlineTrans().apply(kernel) kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0}) result, _ = capsys.readouterr() assert result == ( @@ -7628,12 +7635,21 @@ def test_kern_const_invalid(): kernel = create_kernel("1_single_invoke.f90") kctrans = LFRicKernelConstTrans() + mod_inline_trans = KernelModuleInlineTrans() # Node is not an LFRic kernel with pytest.raises(TransformationError) as excinfo: kctrans.apply(None) assert "Supplied node must be an LFRic kernel" in str(excinfo.value) + # Kernel not module inlined + with pytest.raises(TransformationError) as excinfo: + kctrans.apply(kernel, {"number_of_layers": 1}) + assert "because it is not module inlined" in str(excinfo.value) + + # Module-inline it so we can continue with tests. + mod_inline_trans.apply(kernel) + # Cell shape not quadrilateral with pytest.raises(TransformationError) as excinfo: kctrans.apply(kernel, {"cellshape": "rotund"}) @@ -7687,6 +7703,8 @@ def test_kern_const_invalid_dofs(monkeypatch): monkeypatch.setattr(LFRicKernelConstTrans, "space_to_dofs", {"wa": [], "wb": []}) + KernelModuleInlineTrans().apply(kernel) + with pytest.raises(InternalError) as excinfo: kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0}) assert "Unsupported function space 'w1' found. Expecting one of " \ @@ -7708,6 +7726,9 @@ def dummy(): '''A dummy function that always raises an exception.''' raise NotImplementedError("Monkeypatch error") monkeypatch.setattr(kernel, "get_callees", dummy) + + KernelModuleInlineTrans().apply(kernel) + with pytest.raises(TransformationError) as excinfo: kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0}) assert ( diff --git a/src/psyclone/tests/lfric_quadrature_test.py b/src/psyclone/tests/lfric_quadrature_test.py index 63ef670abc..93aee681f5 100644 --- a/src/psyclone/tests/lfric_quadrature_test.py +++ b/src/psyclone/tests/lfric_quadrature_test.py @@ -78,13 +78,14 @@ def test_field_xyoz(tmpdir): module_declns = ( " use constants_mod\n" - " use field_mod, only : field_proxy_type, field_type\n") + " use field_mod, only : field_proxy_type, field_type\n" + " use testkern_qr_mod, only : testkern_qr_code\n" + ) assert module_declns in generated_code assert ( " subroutine invoke_0_testkern_qr_type(f1, f2, m1, a, m2, istp," " qr)\n" - " use testkern_qr_mod, only : testkern_qr_code\n" " use mesh_mod, only : mesh_type\n" " use function_space_mod, only : BASIS, DIFF_BASIS\n" " use quadrature_xyoz_mod, only : quadrature_xyoz_proxy_type, " @@ -291,11 +292,11 @@ def test_face_qr(tmpdir, dist_mem): module_declns = ( " use constants_mod\n" - " use field_mod, only : field_proxy_type, field_type\n") + " use field_mod, only : field_proxy_type, field_type\n" + " use testkern_qr_faces_mod, only : testkern_qr_faces_code\n") assert module_declns in generated_code - output_decls = (" use testkern_qr_faces_mod, only : " - "testkern_qr_faces_code\n") + output_decls = "" if dist_mem: output_decls += " use mesh_mod, only : mesh_type\n" output_decls += ( diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index a56d19783e..3f6f42c6b9 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -62,7 +62,7 @@ from psyclone.psyir.nodes import ( ACCDataDirective, ACCDirective, ACCEnterDataDirective, ACCKernelsDirective, ACCLoopDirective, ACCParallelDirective, ACCRoutineDirective, - Call, CodeBlock, Directive, Literal, Loop, Node, + Call, CodeBlock, Container, Directive, Literal, Loop, Node, Return, Schedule, PSyDataNode, IntrinsicCall) from psyclone.psyir.nodes.acc_mixins import ACCAsyncMixin from psyclone.psyir.nodes.array_mixin import ArrayMixin @@ -2407,7 +2407,9 @@ def apply(self, node, options=None): ''' Convert the imported variables used inside the kernel into arguments and modify the InvokeSchedule to pass the same imported variables to - the kernel call. + the kernel call. Since it is a pre-requisite that the kernel have been + module-inlined first, this transformation must also update all other + calls to the same module-inlined routine. :param node: a kernel call. :type node: :py:class:`psyclone.psyGen.CodedKern` @@ -2426,6 +2428,9 @@ def apply(self, node, options=None): kernel.symbol_table.precision_datasymbols] count_imported_vars_removed = 0 + # The arguments that we have to add to the Kernel call. + new_kernel_args: list[tuple(str, str)] = [] + # Transform each imported variable into an argument. # TODO #11: When support for logging is added, we could warn the user # if no imports are found in the kernel. @@ -2489,18 +2494,23 @@ def apply(self, node, options=None): f"infrastructure does not have any scalar type equivalent " f"to the PSyIR {updated_sym.datatype} type.") - # Add the imported variable in the call argument list - node.arguments.append(updated_sym.name, go_space) + # Record the addition to the call argument list + new_kernel_args.append((updated_sym.name, go_space)) # Check whether we still need the Container symbol from which # this import was originally accessed - if not kernel.symbol_table.symbols_imported_from(container) and \ - not container.wildcard_import: + if (not kernel.symbol_table.symbols_imported_from(container) and + not container.wildcard_import): kernel.symbol_table.remove(container) - if count_imported_vars_removed > 0: - node.modified = True - + # Since we've modified the Kernel argument list *and* have removed the + # imported symbols from its implementation, we have to make sure we + # update the argument lists of all other calls to the inlined kernel. + for kern in node.ancestor(Container).walk(CodedKern): + if kern.name == node.name: + for arg in new_kernel_args: + kern.arguments.append(arg[0], arg[1]) + kern.modified = True # Create a compatibility layer for all existing Dynamo0p3 transformation # names. These are just derived classes from the new name which print From 0230c2f212e5e83f063b0f5de58247e2f154484b Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 27 Jan 2026 13:43:23 +0000 Subject: [PATCH 11/17] #1823 more test fixing --- ...teration_boundaries_inside_kernel_trans.py | 2 +- src/psyclone/gocean1p0.py | 4 +- .../gocean_opencl_trans_test.py | 47 +++++++------------ .../domain/lfric/lfric_scalar_codegen_test.py | 2 - .../lfric_transformations_test.py | 32 ++++--------- src/psyclone/tests/lfric_basis_test.py | 2 +- src/psyclone/tests/lfric_lma_test.py | 4 +- src/psyclone/tests/lfric_test.py | 6 +-- src/psyclone/tests/psyGen_test.py | 27 ----------- src/psyclone/transformations.py | 1 + 10 files changed, 36 insertions(+), 91 deletions(-) diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index cda3bacb41..c38f5526a8 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -125,7 +125,6 @@ def apply(self, node, options=None): ''' self.validate(node, options) - invoke_sched = node.ancestor(InvokeSchedule) self._boundary_values_declare_and_init(node) # Check that this transformation hasn't already been applied to @@ -267,5 +266,6 @@ def _boundary_values_declare_and_init(self, node): return (inv_xstart, inv_xstop, inv_ystart, inv_ystop) + # For Sphinx AutoAPI documentation generation __all__ = ['GOMoveIterationBoundariesInsideKernelTrans'] diff --git a/src/psyclone/gocean1p0.py b/src/psyclone/gocean1p0.py index 73d8babff7..e5b3459d0d 100644 --- a/src/psyclone/gocean1p0.py +++ b/src/psyclone/gocean1p0.py @@ -1329,9 +1329,7 @@ def psyir_expression(self): pass # Otherwise it's some form of Reference - symbol = self._call.scope.symbol_table.lookup(self.name, otherwise=None) - if not symbol: - import pdb; pdb.set_trace() + symbol = self._call.scope.symbol_table.lookup(self.name) # Gocean field arguments are StructureReferences to the %data attribute if self.argument_type == "field": diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean_opencl_trans_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean_opencl_trans_test.py index acaae7c722..dbd98a0075 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean_opencl_trans_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean_opencl_trans_test.py @@ -300,8 +300,7 @@ def test_invoke_opencl_initialisation(kernel_outputdir, fortran_writer): assert GOceanOpenCLBuild(kernel_outputdir).code_compiles(psy) -@pytest.mark.usefixtures("kernel_outputdir") -def test_invoke_opencl_initialisation_grid(): +def test_invoke_opencl_initialisation_grid(kernel_outputdir): ''' Test that generating OpenCL generation code when there are grid property accesses generated the proper grid on device initialisation code ''' @@ -425,8 +424,7 @@ def test_invoke_opencl_initialisation_grid(): assert "call dx%write_to_device" in candidates assert "call write_grid_buffers(in_fld)" in candidates - # TODO 284: Currently this example cannot be compiled because it needs to - # import a module which won't be found on kernel_outputdir + assert GOceanOpenCLBuild(kernel_outputdir).code_compiles(psy) def test_opencl_routines_initialisation(kernel_outputdir): @@ -645,14 +643,14 @@ def test_psy_init_multiple_kernels(kernel_outputdir, mod_inline_trans): psy, dependencies=["model_mod.f90"]) -def test_psy_init_multiple_devices_per_node(kernel_outputdir, monkeypatch): +def test_psy_init_multiple_devices_per_node(kernel_outputdir, monkeypatch, + mod_inline_trans): ''' Test that we create the appropriate subroutine to initialise an hybrid MPI-OpenCL environment with multiple devices per node. ''' psy, _ = get_invoke("single_invoke.f90", API, idx=0, dist_mem=True) sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): mod_inline_trans.apply(kernel) @@ -688,14 +686,13 @@ def test_psy_init_multiple_devices_per_node(kernel_outputdir, monkeypatch): assert GOceanOpenCLBuild(kernel_outputdir).code_compiles(psy) -def test_psy_init_with_options(kernel_outputdir): +def test_psy_init_with_options(kernel_outputdir, mod_inline_trans): ''' Check that we create a psy_init() routine that sets-up the OpenCL environment with the provided non-default options. ''' psy, _ = get_invoke("single_invoke.f90", API, idx=0) sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): mod_inline_trans.apply(kernel) @@ -713,7 +710,8 @@ def test_psy_init_with_options(kernel_outputdir): @pytest.mark.parametrize("debug_mode", [True, False]) -def test_invoke_opencl_kernel_call(kernel_outputdir, monkeypatch, debug_mode): +def test_invoke_opencl_kernel_call(kernel_outputdir, monkeypatch, debug_mode, + mod_inline_trans): ''' Check that the Invoke OpenCL produce the expected kernel enqueue statement to launch OpenCL kernels. ''' api_config = Config.get().api_conf("gocean") @@ -722,7 +720,6 @@ def test_invoke_opencl_kernel_call(kernel_outputdir, monkeypatch, debug_mode): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): mod_inline_trans.apply(kernel) @@ -786,7 +783,7 @@ def test_invoke_opencl_kernel_call(kernel_outputdir, monkeypatch, debug_mode): @pytest.mark.usefixtures("kernel_outputdir") -def test_opencl_kernel_boundaries_validation(): +def test_opencl_kernel_boundaries_validation(mod_inline_trans): ''' Check that the OpenCL transformation can not be applied if the kernel loop doesn't iterate the whole grid. ''' @@ -805,7 +802,6 @@ def test_opencl_kernel_boundaries_validation(): "GOOpenCLTrans." in str(err.value)) # After move the boundaries the OpenCL transformation should pass - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): mod_inline_trans.apply(kernel) @@ -813,7 +809,7 @@ def test_opencl_kernel_boundaries_validation(): otrans.apply(sched) -def test_opencl_options_validation(): +def test_opencl_options_validation(mod_inline_trans): ''' Check that OpenCL options which are not supported provide appropriate errors. ''' @@ -821,7 +817,6 @@ def test_opencl_options_validation(): sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): mod_inline_trans.apply(kernel) @@ -875,7 +870,8 @@ def test_opencl_options_validation(): @pytest.mark.usefixtures("kernel_outputdir") @pytest.mark.parametrize("option_to_check", ['enable_profiling', 'out_of_order']) -def test_opencl_multi_invoke_options_validation(option_to_check): +def test_opencl_multi_invoke_options_validation(option_to_check, + mod_inline_trans): ''' Check that the OpenCL options constrains are enforced when there are multiple invokes. ''' @@ -884,7 +880,6 @@ def test_opencl_multi_invoke_options_validation(option_to_check): invoke2_schedule = psy.invokes.invoke_list[1].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in invoke1_schedule.coded_kernels(): mod_inline_trans.apply(kernel) @@ -904,7 +899,7 @@ def test_opencl_multi_invoke_options_validation(option_to_check): @pytest.mark.usefixtures("kernel_outputdir") -def test_opencl_options_effects(): +def test_opencl_options_effects(mod_inline_trans): ''' Check that the OpenCL options produce the expected changes in the PSy layer. ''' @@ -913,7 +908,6 @@ def test_opencl_options_effects(): # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): mod_inline_trans.apply(kernel) @@ -982,7 +976,7 @@ def test_opencl_options_effects(): @pytest.mark.parametrize("dist_mem", [True, False]) @pytest.mark.usefixtures("kernel_outputdir") -def test_multiple_command_queues(dist_mem): +def test_multiple_command_queues(dist_mem, mod_inline_trans): ''' Check that barriers (with clFinish) are inserted when a kernel (or a haloexchange in distributed memory) is dispatched to a different queue than its dependency predecessor. ''' @@ -990,7 +984,6 @@ def test_multiple_command_queues(dist_mem): dist_mem=dist_mem) sched = psy.invokes.invoke_list[0].schedule - mod_inline_trans = KernelModuleInlineTrans() # Set the boundaries inside the kernel trans = GOMoveIterationBoundariesInsideKernelTrans() @@ -1028,13 +1021,12 @@ def test_multiple_command_queues(dist_mem): assert kernelbarrier in generated_code -def test_set_kern_args(kernel_outputdir): +def test_set_kern_args(kernel_outputdir, mod_inline_trans): ''' Check that we generate the necessary code to set kernel arguments. ''' psy, _ = get_invoke("single_invoke_two_kernels.f90", API, idx=0) sched = psy.invokes.invoke_list[0].schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): mod_inline_trans.apply(kernel) @@ -1096,14 +1088,13 @@ def test_set_kern_args(kernel_outputdir): assert GOceanOpenCLBuild(kernel_outputdir).code_compiles(psy) -def test_set_kern_args_real_grid_property(tmp_path): +def test_set_kern_args_real_grid_property(kernel_outputdir, mod_inline_trans): ''' Check that we generate correct code to set a real scalar grid property. ''' - psy, _ = get_invoke("driver_test.f90", API, idx=0) - sched = psy.invokes.invoke_list[0].schedule + psy, invoke = get_invoke("driver_test.f90", API, idx=0) + sched = invoke.schedule # Currently, moving the boundaries inside the kernel is a prerequisite # for the GOcean gen_ocl() code generation. - mod_inline_trans = KernelModuleInlineTrans() trans = GOMoveIterationBoundariesInsideKernelTrans() for kernel in sched.coded_kernels(): mod_inline_trans.apply(kernel) @@ -1131,9 +1122,7 @@ def test_set_kern_args_real_grid_property(tmp_path): INTEGER, INTENT(IN), TARGET :: ystart INTEGER, INTENT(IN), TARGET :: ystop''' assert expected in generated_code - # TODO 284: Currently this example cannot be compiled because it needs to - # import a module which won't be found on kernel_outputdir - assert GOceanOpenCLBuild(tmp_path).code_compiles(psy) + assert GOceanOpenCLBuild(kernel_outputdir).code_compiles(psy) def test_set_kern_float_arg(kernel_outputdir, mod_inline_trans): diff --git a/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py b/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py index f97cbf880d..1c79d68bb6 100644 --- a/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_scalar_codegen_test.py @@ -42,8 +42,6 @@ ''' import os -from psyclone.parse.algorithm import parse -from psyclone.psyGen import PSyFactory from psyclone.tests.lfric_build import LFRicBuild from psyclone.tests.utilities import get_invoke diff --git a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py index 4601e3c4fa..11bcf13c63 100644 --- a/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py +++ b/src/psyclone/tests/domain/lfric/transformations/lfric_transformations_test.py @@ -7713,29 +7713,6 @@ def test_kern_const_invalid_dofs(monkeypatch): assert "'wb'" in str(excinfo.value) -def test_kern_const_invalid_kern(monkeypatch): - '''Check that we raise the expected exception when the Fortran to - PSyIR parser fails to parse a kernel. - - ''' - kernel = create_kernel("1_single_invoke.f90") - - kctrans = LFRicKernelConstTrans() - - def dummy(): - '''A dummy function that always raises an exception.''' - raise NotImplementedError("Monkeypatch error") - monkeypatch.setattr(kernel, "get_callees", dummy) - - KernelModuleInlineTrans().apply(kernel) - - with pytest.raises(TransformationError) as excinfo: - kctrans.apply(kernel, {"element_order_h": 0, "element_order_v": 0}) - assert ( - "Failed to parse kernel 'testkern_code'. Error reported was " - "'Monkeypatch error'.") in str(excinfo.value) - - def test_kern_const_invalid_quad(monkeypatch): '''Check that we raise the expected exception when the type of quadrature is not supported by the transformation (we are @@ -7744,6 +7721,9 @@ def test_kern_const_invalid_quad(monkeypatch): ''' kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") + # Kernel has to be module inlined first. + KernelModuleInlineTrans().apply(kernel) + kctrans = LFRicKernelConstTrans() monkeypatch.setattr(kernel, "_eval_shapes", ["gh_quadrature_face"]) with pytest.raises(TransformationError) as excinfo: @@ -7763,6 +7743,9 @@ def test_kern_const_invalid_make_constant1(): ''' kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") + # Kernel has to be module inlined first. + KernelModuleInlineTrans().apply(kernel) + kernel_schedule = kernel.get_callees()[0] symbol_table = kernel_schedule.symbol_table # Make the symbol table's argument list empty. We have to make sure that @@ -7789,6 +7772,9 @@ def test_kern_const_invalid_make_constant2(): ''' kernel = create_kernel("1.1.0_single_invoke_xyoz_qr.f90") + # Kernel has to be module inlined first. + KernelModuleInlineTrans().apply(kernel) + kctrans = LFRicKernelConstTrans() kernel_schedule = kernel.get_callees()[0] symbol_table = kernel_schedule.symbol_table diff --git a/src/psyclone/tests/lfric_basis_test.py b/src/psyclone/tests/lfric_basis_test.py index 2a6e8c0c50..1149e64f4b 100644 --- a/src/psyclone/tests/lfric_basis_test.py +++ b/src/psyclone/tests/lfric_basis_test.py @@ -191,10 +191,10 @@ def test_single_kern_eval(tmpdir): # Check module declarations assert "use constants_mod\n" in code assert "use field_mod, only : field_proxy_type, field_type" in code + assert "use testkern_eval_mod, only : testkern_eval_code" in code # Check subroutine declarations assert " subroutine invoke_0_testkern_eval_type(f0, cmap)" in code - assert " use testkern_eval_mod, only : testkern_eval_code" in code assert " use function_space_mod, only : BASIS, DIFF_BASIS" in code assert " type(field_type), intent(in) :: f0" in code assert " type(field_type), intent(in) :: cmap" in code diff --git a/src/psyclone/tests/lfric_lma_test.py b/src/psyclone/tests/lfric_lma_test.py index d2c23b2345..a2a351c989 100644 --- a/src/psyclone/tests/lfric_lma_test.py +++ b/src/psyclone/tests/lfric_lma_test.py @@ -504,14 +504,14 @@ def test_operator_different_spaces(tmpdir): use constants_mod use operator_mod, only : operator_proxy_type, operator_type use field_mod, only : field_proxy_type, field_type + use assemble_weak_derivative_w3_w2_kernel_mod, only : \ +assemble_weak_derivative_w3_w2_kernel_code implicit none public contains subroutine invoke_0_assemble_weak_derivative_w3_w2_kernel_type(mapping, \ coord, qr) - use assemble_weak_derivative_w3_w2_kernel_mod, only : \ -assemble_weak_derivative_w3_w2_kernel_code use mesh_mod, only : mesh_type use function_space_mod, only : BASIS, DIFF_BASIS use quadrature_xyoz_mod, only : quadrature_xyoz_proxy_type, \ diff --git a/src/psyclone/tests/lfric_test.py b/src/psyclone/tests/lfric_test.py index fac2ff303b..337d9d618f 100644 --- a/src/psyclone/tests/lfric_test.py +++ b/src/psyclone/tests/lfric_test.py @@ -4305,7 +4305,7 @@ def test_read_only_fields_hex(tmpdir): assert expected in generated_code -def test_mixed_precision_args(tmpdir): +def test_mixed_precision_args(tmp_path): ''' Test that correct code is generated for the PSy-layer when there are scalars, fields and operators with different precision @@ -4322,6 +4322,7 @@ def test_mixed_precision_args(tmpdir): assert """ use field_mod, only : field_proxy_type, field_type use operator_mod, only : operator_proxy_type, operator_type + use mixed_kernel_mod, only : mixed_code use r_solver_field_mod, only : r_solver_field_proxy_type, r_solver_field_type use r_solver_operator_mod, only : r_solver_operator_proxy_type, \ r_solver_operator_type @@ -4336,7 +4337,6 @@ def test_mixed_precision_args(tmpdir): subroutine invoke_0(scalar_r_def, field_r_def, operator_r_def, \ scalar_r_solver, field_r_solver, operator_r_solver, scalar_r_tran, \ field_r_tran, operator_r_tran, scalar_r_bl, field_r_bl) - use mixed_kernel_mod, only : mixed_code use mesh_mod, only : mesh_type real(kind=r_def), intent(in) :: scalar_r_def type(field_type), intent(in) :: field_r_def @@ -4388,7 +4388,7 @@ def test_mixed_precision_args(tmpdir): """ in generated_code # Test compilation - assert LFRicBuild(tmpdir).code_compiles(psy) + assert LFRicBuild(tmp_path).code_compiles(psy) def test_lfricpsy_gen_container_routines(tmpdir): diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index aaa6f55f94..39665cff53 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -82,7 +82,6 @@ from psyclone.tests.test_files.dummy_transformations import LocalTransformation from psyclone.tests.utilities import get_invoke from psyclone.transformations import (LFRicRedundantComputationTrans, - LFRicKernelConstTrans, LFRicColourTrans, LFRicOMPLoopTrans, Transformation) @@ -548,7 +547,6 @@ def test_derived_type_deref_naming(tmpdir): output = ( " subroutine invoke_0_testkern_type" "(a, f1_my_field, f1_my_field_1, m1, m2)\n" - " use testkern_mod, only : testkern_code\n" " use mesh_mod, only : mesh_type\n" " real(kind=r_def), intent(in) :: a\n" " type(field_type), intent(in) :: f1_my_field\n" @@ -2107,31 +2105,6 @@ def test_dataaccess_same_vector_indices(monkeypatch): "never happen" in str(excinfo.value)) -def test_modified_kern_line_length(kernel_outputdir, monkeypatch): - '''Modified Fortran kernels are written to file linewrapped at 132 - characters. This test checks that this linewrapping works. - - ''' - psy, invoke = get_invoke("1_single_invoke.f90", api="lfric", idx=0) - sched = invoke.schedule - kernels = sched.walk(Kern) - # This example does not conform to the _code, _mod - # convention so monkeypatch it to avoid the PSyIR code generation - # raising an exception. This limitation is the subject of issue - # #520. - monkeypatch.setattr(kernels[0], "_module_name", "testkern_mod") - ktrans = LFRicKernelConstTrans() - ktrans.apply(kernels[0], {"number_of_layers": 100}) - # Generate the code (this triggers the generation of new kernels) - _ = str(psy.gen) - filepath = os.path.join(str(kernel_outputdir), "testkern_0_mod.f90") - assert os.path.isfile(filepath) - # Check that the argument list is line wrapped as it is longer - # than 132 characters. - with open(filepath, encoding="utf-8") as testfile: - assert "map_w2, &\n&ndf_w3" in testfile.read() - - def test_walk(fortran_reader): '''Tests the walk functionality.''' diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 3f6f42c6b9..6ccb65ebd7 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -2512,6 +2512,7 @@ def apply(self, node, options=None): kern.arguments.append(arg[0], arg[1]) kern.modified = True + # Create a compatibility layer for all existing Dynamo0p3 transformation # names. These are just derived classes from the new name which print # a deprecation message when creating an instance From 240f86de1ccbc371ac68080d896c039cdd06de5a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 27 Jan 2026 14:37:45 +0000 Subject: [PATCH 12/17] #1823 WIP fixing examples [skip ci] --- examples/gocean/eg1/opencl_transformation.py | 20 ++++++++++++-------- examples/gocean/eg3/ocl_trans.py | 8 ++++++-- examples/gocean/eg4/acc_transform.py | 2 +- examples/gocean/eg4/ocl_transform.py | 13 ++++++++----- examples/lfric/eg14/Makefile | 19 +++++-------------- examples/lfric/eg14/acc_parallel.py | 4 ++++ 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/examples/gocean/eg1/opencl_transformation.py b/examples/gocean/eg1/opencl_transformation.py index 6c2011473d..682f59037f 100644 --- a/examples/gocean/eg1/opencl_transformation.py +++ b/examples/gocean/eg1/opencl_transformation.py @@ -36,24 +36,26 @@ ''' Module providing a PSyclone transformation script that converts the Schedule of each Invoke to use OpenCL. ''' -from psyclone.psyGen import TransInfo, InvokeSchedule -from psyclone.domain.gocean.transformations import GOOpenCLTrans, \ - GOMoveIterationBoundariesInsideKernelTrans +from psyclone.psyGen import InvokeSchedule +from psyclone.domain.common.transformations import KernelModuleInlineTrans +from psyclone.domain.gocean.transformations import ( + GOOpenCLTrans, GOMoveIterationBoundariesInsideKernelTrans) +from psyclone.psyir.nodes import FileContainer +from psyclone.transformations import KernelImportsToArguments -def trans(psyir): +def trans(psyir: FileContainer): ''' Transformation routine for use with PSyclone. Converts any imported- variable accesses into kernel arguments and then applies the OpenCL transformation to the PSy layer. :param psyir: the PSyIR of the PSy-layer. - :type psyir: :py:class:`psyclone.psyir.nodes.FileContainer` ''' # Get the necessary transformations - tinfo = TransInfo() - import_trans = tinfo.get_trans_name('KernelImportsToArguments') + import_trans = KernelImportsToArguments() + mod_inline_trans = KernelModuleInlineTrans() move_boundaries_trans = GOMoveIterationBoundariesInsideKernelTrans() cltrans = GOOpenCLTrans() @@ -67,9 +69,11 @@ def trans(psyir): continue # Remove the imports from inside each kernel and move PSy-layer - # loop boundaries inside the kernel as a mask. + # loop boundaries inside the kernel as a mask. To do this we must + # first module-inline the kernel into the PSy layer module. for kern in schedule.kernels(): print("Update kernel: " + kern.name) + mod_inline_trans.apply(kern) move_boundaries_trans.apply(kern) import_trans.apply(kern) diff --git a/examples/gocean/eg3/ocl_trans.py b/examples/gocean/eg3/ocl_trans.py index f298eb3ec2..1f3f127aa8 100644 --- a/examples/gocean/eg3/ocl_trans.py +++ b/examples/gocean/eg3/ocl_trans.py @@ -39,24 +39,28 @@ from psyclone.psyGen import InvokeSchedule from psyclone.psyir.transformations import ( FoldConditionalReturnExpressionsTrans) +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.gocean.transformations import ( GOOpenCLTrans, GOMoveIterationBoundariesInsideKernelTrans) +from psyclone.psyir.nodes import FileContainer -def trans(psyir): +def trans(psyir: FileContainer): ''' Applies OpenCL to the given PSy-layer. :param psyir: the PSyIR of the PSy-layer. - :type psyir: :py:class:`psyclone.psyir.nodes.FileContainer` ''' + mod_inline_trans = KernelModuleInlineTrans() ocl_trans = GOOpenCLTrans() fold_trans = FoldConditionalReturnExpressionsTrans() move_boundaries_trans = GOMoveIterationBoundariesInsideKernelTrans() # Provide kernel-specific OpenCL optimization options for idx, kern in enumerate(psyir.kernels()): + # Kernel has to be module-inlined first. + mod_inline_trans.apply(kern) # Move the PSy-layer loop boundaries inside the kernel as a kernel # mask, this allows to iterate through the whole domain move_boundaries_trans.apply(kern) diff --git a/examples/gocean/eg4/acc_transform.py b/examples/gocean/eg4/acc_transform.py index c7be9c4bff..83419d6b3c 100644 --- a/examples/gocean/eg4/acc_transform.py +++ b/examples/gocean/eg4/acc_transform.py @@ -77,7 +77,7 @@ def trans(psyir): # Convert any accesses to imported data into kernel arguments, put an # 'acc routine' directive inside, and module-inline each kernel for kern in schedule.coded_kernels(): + itrans.apply(kern) if kern.name == "kern_use_var_code": g2localtrans.apply(kern) ktrans.apply(kern) - itrans.apply(kern) diff --git a/examples/gocean/eg4/ocl_transform.py b/examples/gocean/eg4/ocl_transform.py index 9bb0c91f48..3ae354a076 100644 --- a/examples/gocean/eg4/ocl_transform.py +++ b/examples/gocean/eg4/ocl_transform.py @@ -39,22 +39,25 @@ ''' from psyclone.transformations import KernelImportsToArguments -from psyclone.domain.gocean.transformations import GOOpenCLTrans, \ - GOMoveIterationBoundariesInsideKernelTrans +from psyclone.domain.common.transformations import KernelModuleInlineTrans +from psyclone.domain.gocean.transformations import ( + GOOpenCLTrans, GOMoveIterationBoundariesInsideKernelTrans) +from psyclone.psyir.nodes import FileContainer -def trans(psyir): +def trans(psyir: FileContainer): ''' Transformation routine for use with PSyclone. Applies the OpenCL - transform to the first Invoke in the psy object. + transform to the first Invoke in the PSy-layer. :param psyir: the PSyIR of the PSy-layer. - :type psyir: :py:class:`psyclone.psyir.nodes.FileContainer` ''' # Convert any kernel accesses to imported data into arguments + mod_inline_trans = KernelModuleInlineTrans() ktrans = KernelImportsToArguments() for kern in psyir.kernels(): + mod_inline_trans.apply(kern) ktrans.apply(kern) # Provide kernel-specific OpenCL optimization options diff --git a/examples/lfric/eg14/Makefile b/examples/lfric/eg14/Makefile index 4e8ff6ba21..dfc379e3ac 100644 --- a/examples/lfric/eg14/Makefile +++ b/examples/lfric/eg14/Makefile @@ -46,14 +46,12 @@ PSYROOT=../../.. include ../lfric_common.mk GENERATED_FILES = *.o *.mod $(EXEC) main_alg.f90 main_psy.f90 \ - other_alg_mod_psy.f90 other_alg_mod_alg.f90 \ - testkern_w0_kernel_?_mod.f90 + other_alg_mod_psy.f90 other_alg_mod_alg.f90 F90 ?= gfortran F90FLAGS ?= -Wall -g -OBJ = main_psy.o main_alg.o other_alg_mod_psy.o other_alg_mod_alg.o \ - testkern_w0_kernel_0_mod.o +OBJ = main_psy.o main_alg.o other_alg_mod_psy.o other_alg_mod_alg.o EXEC = example_openacc @@ -70,13 +68,8 @@ PROFILE_LINK=-ldummy .PHONY: transform compile run -# This makefile assumes that the transformed kernel will be named -# 'testkern_w0_kernel_0_mod.f90'. However, if it already exists then PSyclone -# will create 'testkern_..._1_mod.f90' so remove it first. Also remove -# main_psy.f90, since testkern_w0_kernel_0_mod will not be recreated -# if main_psy.f90 exists transform: - rm -f testkern_w0_kernel_0_mod.f90 main_psy.f90 + rm -f main_psy.f90 ${MAKE} main_psy.f90 ${MAKE} other_alg_mod_psy.f90 @@ -86,8 +79,6 @@ transform: ${PSYCLONE} -api lfric -dm -s ./acc_parallel.py --profile invokes \ -opsy $*_psy.f90 -oalg $*_alg.f90 $< -testkern_w0_kernel_0_mod.f90: main_psy.f90 - compile: transform ${EXEC} run: compile @@ -101,8 +92,8 @@ $(PROFILE_LIB): $(MAKE) -C $(PROFILE_PATH) # Dependencies -main_psy.o: other_alg_mod_psy.o testkern_w0_kernel_0_mod.o -main_alg.o: other_alg_mod_alg.o main_psy.o testkern_w0_kernel_0_mod.o +main_psy.o: other_alg_mod_psy.o +main_alg.o: other_alg_mod_alg.o main_psy.o %.o: %.F90 $(F90) $(F90FLAGS) -I$(PROFILE_PATH) -c $< diff --git a/examples/lfric/eg14/acc_parallel.py b/examples/lfric/eg14/acc_parallel.py index cb9352f1d2..781373ba46 100644 --- a/examples/lfric/eg14/acc_parallel.py +++ b/examples/lfric/eg14/acc_parallel.py @@ -40,6 +40,7 @@ -s option. ''' +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.lfric import LFRicConstants from psyclone.psyGen import CodedKern, InvokeSchedule from psyclone.psyir.transformations import ACCKernelsTrans @@ -60,6 +61,7 @@ def trans(psyir): ctrans = LFRicColourTrans() enter_data_trans = ACCEnterDataTrans() + mod_inline_trans = KernelModuleInlineTrans() kernel_trans = ACCKernelsTrans() rtrans = ACCRoutineTrans() @@ -86,4 +88,6 @@ def trans(psyir): # adds '!$acc routine' which ensures the kernel is compiled for the # OpenACC device. for kernel in subroutine.walk(CodedKern): + # Module inlining is a pre-requisite for kernel transformations. + mod_inline_trans.apply(kernel) rtrans.apply(kernel) From b269f7a739620e37078a4f2e7724ef685eea6a17 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 27 Jan 2026 16:39:10 +0000 Subject: [PATCH 13/17] #1823 add docstrings to new mixin --- .../kernel_transformation_mixin.py | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py b/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py index 6c9631ee9f..80f521074b 100644 --- a/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py +++ b/src/psyclone/domain/common/transformations/kernel_transformation_mixin.py @@ -1,3 +1,43 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2026, Science and Technology Facilities Council. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Author: A. R. Porter, STFC Daresbury Lab + +""" +This module provides the KernelTransformationMixin class. + +""" + from typing import Union from psyclone.psyGen import CodedKern @@ -8,10 +48,27 @@ class KernelTransformationMixin: - """ """ + """ + A mixin class to be used by all Transformations that act upon PSyKAl + Kernels. + + Provides functionality to check that a Kernel has been module-inlined + before subsequent transformations are applied to it. + + """ + def _check_kernel_is_local(self, node: Union[Node, CodedKern]) -> None: + """ + Check that the supplied kernel node has been module inlined. + + :param node: the PSyKAl Kernel to check. + + :raises TransformationError: if the supplied Kernel has not been + module inlined. + :raises TransformationError: if the supplied Kernel (call) is not + within a Container or the Container does not contain the + implementation of the Kernel. - def _check_kernel_is_local(self, node: Union[Node, CodedKern]): - """ """ + """ if not isinstance(node, CodedKern): return From 9d8ac4ab0d4fe80e8ccc24c52cdef8d329417cbd Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 2 Feb 2026 21:37:41 +0000 Subject: [PATCH 14/17] #1823 begin adding new tests [skip ci] --- src/psyclone/psyGen.py | 17 ++--- .../kernel_transformation_mixin_test.py | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 src/psyclone/tests/domain/common/transformations/kernel_transformation_mixin_test.py diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index 4cd46e89f8..eefee783ee 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -1364,12 +1364,7 @@ def lower_to_language_level(self) -> Node: ''' symtab = self.ancestor(InvokeSchedule).symbol_table - rsymbol = symtab.lookup(self._name, otherwise=None) - if not rsymbol: - raise GenerationError( - f"Cannot lower this Kernel call to '{self.name}' " - f"because no corresponding Symbol exists in this module.") - + rsymbol = symtab.lookup(self._name) # Create Call to the rsymbol with the argument expressions as children # of the new node call_node = Call.create(rsymbol, self.arguments.psyir_expressions()) @@ -1378,13 +1373,13 @@ def lower_to_language_level(self) -> Node: self.replace_with(call_node) return call_node - def incremented_arg(self): - ''' Returns the argument that has INC access. Raises a - FieldNotFoundError if none is found. + def incremented_arg(self) -> str: + ''' Returns the argument that has INC access. - :rtype: str - :raises FieldNotFoundError: if none is found. :returns: a Fortran argument name. + + :raises FieldNotFoundError: if no incremented argument is found. + ''' for arg in self.arguments.args: if arg.access == AccessType.INC: diff --git a/src/psyclone/tests/domain/common/transformations/kernel_transformation_mixin_test.py b/src/psyclone/tests/domain/common/transformations/kernel_transformation_mixin_test.py new file mode 100644 index 0000000000..f1cc61ea3d --- /dev/null +++ b/src/psyclone/tests/domain/common/transformations/kernel_transformation_mixin_test.py @@ -0,0 +1,64 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2026, Science and Technology Facilities Council. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Author: A. R. Porter, STFC Daresbury Lab +# ----------------------------------------------------------------------------- + +""" +Contains py.test tests for the KernelTransformationMixin class. + +""" + +import pytest + +from psyclone.domain.common.transformations.kernel_transformation_mixin import\ + KernelTransformationMixin +from psyclone.transformations import Transformation +from psyclone.psyir.transformations.transformation_error import ( + TransformationError) + + +class MyTransform(Transformation, KernelTransformationMixin): + ''' + A dummy transformation for testing the KernelTransformationMixin. + ''' + def apply(self, node): + ''' + ''' + + +def test_check_kernel_is_local(): + ''' + ''' + my_trans = MyTransform() + my_trans._check_kernel_is_local(None) From 2e0dbc12e8c0a686376f7a9c13a72bbc3b2272c8 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 4 Feb 2026 11:05:56 +0000 Subject: [PATCH 15/17] #1823 get coverage of new mixin --- .../kernel_transformation_mixin_test.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/psyclone/tests/domain/common/transformations/kernel_transformation_mixin_test.py b/src/psyclone/tests/domain/common/transformations/kernel_transformation_mixin_test.py index f1cc61ea3d..e97932871f 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_transformation_mixin_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_transformation_mixin_test.py @@ -41,11 +41,16 @@ import pytest +from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.domain.common.transformations.kernel_transformation_mixin import\ KernelTransformationMixin -from psyclone.transformations import Transformation +from psyclone.psyGen import CodedKern +from psyclone.psyir.nodes import Call, Container +from psyclone.psyir.symbols import RoutineSymbol from psyclone.psyir.transformations.transformation_error import ( TransformationError) +from psyclone.tests.utilities import get_invoke +from psyclone.transformations import Transformation class MyTransform(Transformation, KernelTransformationMixin): @@ -59,6 +64,41 @@ def apply(self, node): def test_check_kernel_is_local(): ''' + Tests for the _check_kernel_is_local method. + ''' my_trans = MyTransform() - my_trans._check_kernel_is_local(None) + # Just returns None for anything other than a CodedKern + assert my_trans._check_kernel_is_local(None) is None + a_call = Call.create(RoutineSymbol("sub")) + assert my_trans._check_kernel_is_local(a_call) is None + psy, invoke = get_invoke("single_invoke_three_kernels.f90", api="gocean", + idx=0) + kern = invoke.schedule.walk(CodedKern)[0] + with pytest.raises(TransformationError) as err: + my_trans._check_kernel_is_local(kern) + assert ("Cannot transform this Kernel call to 'compute_cu_code' because " + "it is not module inlined" in str(err.value)) + mod_inline_trans = KernelModuleInlineTrans() + mod_inline_trans.apply(kern) + # Check should now pass. + my_trans._check_kernel_is_local(kern) + # We now want to test the checks for edge cases. + sym = kern.scope.symbol_table.lookup(kern.name) + # Find the newly-inlined kernel routine. + container = invoke.schedule.ancestor(Container) + routine = container.find_routine_psyir(kern.name, allow_private=True) + # Remove the routine but ensure its symbol can still be found. + routine.detach() + container.symbol_table.add(sym) + with pytest.raises(TransformationError) as err: + my_trans._check_kernel_is_local(kern) + assert ("ancestor Container does not contain a Routine named " + "'compute_cu_code'" in str(err.value)) + # Detach the invoke routine from its parent Container but make sure + # we copy in the RoutineSymbol to get past the first check + invoke.schedule.detach() + invoke.schedule.symbol_table.add(sym) + with pytest.raises(TransformationError) as err: + my_trans._check_kernel_is_local(kern) + assert "there is no ancestor Container in which to look" in str(err.value) From f73cd68ee72a4342bc61cfb5fe31a7f996708b93 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 4 Feb 2026 13:04:16 +0000 Subject: [PATCH 16/17] #1823 remove unused/unreachable code and reinstate some tests --- ...teration_boundaries_inside_kernel_trans.py | 12 ++- src/psyclone/psyGen.py | 98 +------------------ .../kernel_module_inline_trans_test.py | 37 +++++++ src/psyclone/transformations.py | 7 +- 4 files changed, 52 insertions(+), 102 deletions(-) diff --git a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py index c38f5526a8..ed6f2bc545 100644 --- a/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py +++ b/src/psyclone/domain/gocean/transformations/gocean_move_iteration_boundaries_inside_kernel_trans.py @@ -206,8 +206,18 @@ def apply(self, node, options=None): if_statement = IfBlock.create(condition, [Return()]) kschedule.children.insert(0, if_statement) - def _boundary_values_declare_and_init(self, node): + def _boundary_values_declare_and_init( + self, node: GOKern) -> tuple[DataSymbol, DataSymbol, + DataSymbol, DataSymbol]: ''' + Declare and initialise the loop boundary values required for + the supplied kernel. + + :param node: the GOcean kernel for which the loop boundaries are + required. + + :returns: a tuple of the DataSymbols representing the x-start, x-stop, + y-start and y-stop loop limits, in that order. ''' # Get useful references invoke_st = node.ancestor(InvokeSchedule).symbol_table diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index eefee783ee..64019f8242 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -68,7 +68,6 @@ from psyclone.psyir.symbols import ( ArgumentInterface, ArrayType, ContainerSymbol, DataSymbol, ScalarType, UnresolvedType, ImportInterface, INTEGER_TYPE, RoutineSymbol) -from psyclone.psyir.symbols.datatypes import UnsupportedFortranType from psyclone.psyir.symbols.symbol_table import SymbolTable # The types of 'intent' that an argument to a Fortran subroutine @@ -1105,27 +1104,16 @@ def arguments(self): return self._arguments @property - def name(self): + def name(self) -> str: ''' :returns: the name of the kernel. - :rtype: str ''' return self._name - @name.setter - def name(self, value): - ''' - Set the name of the kernel. - - :param str value: The name of the kernel. - ''' - self._name = value - - def is_coloured(self): + def is_coloured(self) -> bool: ''' - :returns: True if this kernel is being called from within a \ + :returns: True if this kernel is being called from within a coloured loop. - :rtype: bool ''' parent_loop = self.ancestor(Loop) while parent_loop: @@ -1413,86 +1401,6 @@ def ast(self): self._fp2_ast = my_parser(reader) return self._fp2_ast - @staticmethod - def _new_name(original, tag, suffix): - ''' - Construct a new name given the original, a tag and a suffix (which - may or may not terminate the original name). If suffix is present - in the original name then the `tag` is inserted before it. - - :param str original: The original name - :param str tag: Tag to insert into new name - :param str suffix: Suffix with which to end new name. - :returns: New name made of original + tag + suffix - :rtype: str - ''' - if original.endswith(suffix): - return original[:-len(suffix)] + tag + suffix - return original + tag + suffix - - def _rename_psyir(self, suffix): - '''Rename the PSyIR module and kernel names by adding the supplied - suffix to the names. This change affects the KernCall and - KernelSchedule nodes as well as the kernel metadata declaration. - - :param str suffix: the string to insert into the quantity names. - - ''' - # We need to get the kernel schedule before modifying self.name. - kern_schedules = self.get_callees() - container = kern_schedules[0].ancestor(Container) - - # Use the suffix to create a new kernel name. This will - # conform to the PSyclone convention of ending in "_code" - orig_mod_name = self.module_name[:] - new_mod_name = self._new_name(orig_mod_name, suffix, "_mod") - - # If the kernel is polymorphic, we can just change the name of - # the interface. - interface_sym = self.get_interface_symbol() - if interface_sym: - orig_kern_name = interface_sym.name - new_kern_name = self._new_name(orig_kern_name, suffix, "_code") - container.symbol_table.rename_symbol(interface_sym, new_kern_name) - self.name = new_kern_name - else: - kern_schedule = kern_schedules[0] - orig_kern_name = kern_schedule.name[:] - new_kern_name = self._new_name(orig_kern_name, suffix, "_code") - - # Change the name of this kernel and the associated - # module. These names are used when generating the PSy-layer. - self.name = new_kern_name[:] - kern_schedule.name = new_kern_name[:] - - self._module_name = new_mod_name[:] - container.name = new_mod_name[:] - - # Ensure the metadata points to the correct procedure now. Since this - # routine is general purpose, we won't always have a domain-specific - # Container here and if we don't, it won't have a 'metadata' property. - if hasattr(container, "metadata"): - container.metadata.procedure_name = new_kern_name[:] - # TODO #928 - until the LFRic KernelInterface is fully functional, we - # can't raise language-level PSyIR to LFRic and therefore we have to - # manually fix the name of the procedure within the text that stores - # the kernel metadata. - container_table = container.symbol_table - for sym in container_table.datatypesymbols: - if isinstance(sym.datatype, UnsupportedFortranType): - # If the DataTypeSymbol is a KernelMetadata Type, change its - # kernel code name - for line in sym.datatype.declaration.split('\n'): - if "PROCEDURE," in line: - newl = f"PROCEDURE, NOPASS :: code => {new_kern_name}" - new_declaration = sym.datatype.declaration.replace( - line, newl) - # pylint: disable=protected-access - sym._datatype = UnsupportedFortranType( - new_declaration, - partial_datatype=sym.datatype.partial_datatype) - break # There is only one such statement per type - @property def modified(self): ''' diff --git a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py index f5f08c0a6d..d66089e6c1 100644 --- a/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py +++ b/src/psyclone/tests/domain/common/transformations/kernel_module_inline_trans_test.py @@ -209,6 +209,43 @@ def test_validate_no_inline_global_var(parser): inline_trans.validate(kernels[0]) +def test_validate_name_clashes(): + ''' Test that if the module-inline transformation finds the kernel name + already used in the Container scope, it raises the appropriate error''' + # Use LFRic example with a repeated CodedKern + psy, _ = get_invoke("4.6_multikernel_invokes.f90", "lfric", idx=0, + dist_mem=False) + schedule = psy.invokes.invoke_list[0].schedule + coded_kern = schedule.children[0].loop_body[0] + inline_trans = KernelModuleInlineTrans() + + # Check that name clashes which are not subroutines are detected + schedule.symbol_table.add(DataSymbol("ru_code", REAL_TYPE)) + with pytest.raises(TransformationError) as err: + inline_trans.apply(coded_kern) + assert ("Cannot module-inline Kernel 'ru_code' because symbol " + "'ru_code: DataSymbol, Automatic>' with " + "the same name already exists and changing the name of " + "module-inlined subroutines is not supported yet." + in str(err.value)) + + # TODO # 898. Manually force removal of previous added symbol. + # symbol_table.remove() is not implemented yet. + schedule.symbol_table._symbols.pop("ru_code") + # Also remove the RoutineSymbol representing the Kernel that is + # automatically added to the Container during PSy-layer construction. + schedule.ancestor(Container).symbol_table._symbols.pop("ru_code") + # Check that if a subroutine with the same name already exists and it is + # not identical, it fails. + schedule.parent.addchild(Routine.create("ru_code")) + with pytest.raises(TransformationError) as err: + inline_trans.apply(coded_kern) + assert ("Kernel 'ru_code' cannot be module inlined into Container " + "'multikernel_invokes_7_psy' because a *different* routine with " + "that name already exists and versioning of module-inlined " + "subroutines is not implemented yet.") in str(err.value) + + def test_validate_unsupported_symbol_shadowing(fortran_reader, monkeypatch): ''' Test that the validate method refuses to transform a kernel which contains local variables that shadow a module name that would need to diff --git a/src/psyclone/transformations.py b/src/psyclone/transformations.py index 6ccb65ebd7..50f35bf124 100644 --- a/src/psyclone/transformations.py +++ b/src/psyclone/transformations.py @@ -1801,12 +1801,7 @@ def make_constant(symbol_table, arg_position, value, arg_list_info = KernCallArgList(kernel) arg_list_info.generate() - try: - kernel_schedules = kernel.get_callees() - except NotImplementedError as excinfo: - raise TransformationError( - f"Failed to parse kernel '{kernel.name}'. Error reported was " - f"'{excinfo}'.") from excinfo + kernel_schedules = kernel.get_callees() for kernel_schedule in kernel_schedules: symbol_table = kernel_schedule.symbol_table From 2eaf62c199747261561f2f52e9741666a8364b4f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 4 Feb 2026 17:09:54 +0000 Subject: [PATCH 17/17] #1823 allow for ContainerSymbol from which kernel is imported to be in outer scope --- src/psyclone/psyir/nodes/extract_node.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/psyclone/psyir/nodes/extract_node.py b/src/psyclone/psyir/nodes/extract_node.py index 19ff94dd4c..5dd91ed294 100644 --- a/src/psyclone/psyir/nodes/extract_node.py +++ b/src/psyclone/psyir/nodes/extract_node.py @@ -470,7 +470,9 @@ def bring_external_symbols(read_write_info: "ReadWriteInfo", continue container = symbol_table.find_or_create( module_name, symbol_type=ContainerSymbol) - + # Take care in case we've found an existing ContainerSymbol and + # it's in an outer scope. + actual_table = container.find_symbol_table(symbol_table.node) # Now look up the original symbol. While the variable could # be declared Unresolved here (i.e. just imported), we need the # type information for the output variables (VAR_post), which @@ -493,7 +495,7 @@ def bring_external_symbols(read_write_info: "ReadWriteInfo", else: interface = ImportInterface(container) - symbol_table.find_or_create_tag( + actual_table.find_or_create_tag( tag=f"{signature[0]}@{module_name}", root_name=signature[0], symbol_type=DataSymbol, interface=interface, datatype=container_symbol.datatype)