From 52b962a230e6c932fba4e7936197ab166ebd8c11 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Fri, 13 Mar 2026 13:21:28 +0000 Subject: [PATCH 01/25] Changes to OpenMP scripts to extract arguments from iom_put --- examples/nemo/scripts/omp_cpu_trans.py | 7 ++++++- examples/nemo/scripts/omp_gpu_trans.py | 9 +++++++-- examples/nemo/scripts/utils.py | 20 ++++++++++++++++---- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/examples/nemo/scripts/omp_cpu_trans.py b/examples/nemo/scripts/omp_cpu_trans.py index 7c5713b404..43a0176744 100755 --- a/examples/nemo/scripts/omp_cpu_trans.py +++ b/examples/nemo/scripts/omp_cpu_trans.py @@ -40,8 +40,9 @@ import os from utils import ( insert_explicit_loop_parallelism, normalise_loops, add_profiling, + iom_put_argument_to_temporary, PARALLELISATION_ISSUES, NEMO_MODULES_TO_IMPORT) -from psyclone.psyir.nodes import Routine +from psyclone.psyir.nodes import Routine, Call from psyclone.transformations import OMPLoopTrans # Enable the insertion of profiling hooks during the transformation script @@ -107,6 +108,10 @@ def trans(psyir): for subroutine in psyir.walk(Routine): print(f"Adding OpenMP threading to subroutine: {subroutine.name}") + # Extract any array operations from iom_put calls to temporary + # expressions that can be parallelised. + iom_put_argument_to_temporary(subroutine.walk(Call)) + if PROFILING_ENABLED: add_profiling(subroutine.children) diff --git a/examples/nemo/scripts/omp_gpu_trans.py b/examples/nemo/scripts/omp_gpu_trans.py index e0579f4c7c..50887679f3 100755 --- a/examples/nemo/scripts/omp_gpu_trans.py +++ b/examples/nemo/scripts/omp_gpu_trans.py @@ -40,8 +40,9 @@ import os from utils import ( add_profiling, inline_calls, insert_explicit_loop_parallelism, - normalise_loops, PARALLELISATION_ISSUES, NEMO_MODULES_TO_IMPORT) -from psyclone.psyir.nodes import Routine, Loop + normalise_loops, iom_put_argument_to_temporary, + PARALLELISATION_ISSUES, NEMO_MODULES_TO_IMPORT) +from psyclone.psyir.nodes import Routine, Loop, Call from psyclone.psyir.transformations import ( OMPTargetTrans, OMPDeclareTargetTrans) from psyclone.transformations import ( @@ -199,6 +200,10 @@ def trans(psyir): if "pp_len" not in symtab: symtab.add(symtab.lookup("pp_len")) + # Extract any array operations from iom_put calls to temporary + # expressions that can be parallelised. + iom_put_argument_to_temporary(subroutine.walk(Call)) + normalise_loops( subroutine, hoist_local_arrays=False, diff --git a/examples/nemo/scripts/utils.py b/examples/nemo/scripts/utils.py index 43a8dd9ffc..eb14d73965 100755 --- a/examples/nemo/scripts/utils.py +++ b/examples/nemo/scripts/utils.py @@ -41,14 +41,15 @@ from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.psyir.nodes import ( Assignment, Loop, Directive, Node, Reference, CodeBlock, Call, - Routine, Schedule, IntrinsicCall, StructureReference, IfBlock) -from psyclone.psyir.symbols import DataSymbol + Routine, Schedule, IntrinsicCall, StructureReference, IfBlock, + Operation) +from psyclone.psyir.symbols import DataSymbol, ArrayType from psyclone.psyir.transformations import ( ArrayAssignment2LoopsTrans, HoistLoopBoundExprTrans, HoistLocalArraysTrans, HoistTrans, InlineTrans, Maxval2LoopTrans, ProfileTrans, OMPMinimiseSyncTrans, Reference2ArrayRangeTrans, - ScalarisationTrans, IncreaseRankLoopArraysTrans, MaximalRegionTrans) -from psyclone.transformations import TransformationError + ScalarisationTrans, IncreaseRankLoopArraysTrans, MaximalRegionTrans, + DataNodeToTempTrans, TransformationError) # USE statements to chase to gather additional symbol information. NEMO_MODULES_TO_IMPORT = [ @@ -526,3 +527,14 @@ def _satisfies_minimum_region_rules(self, region: list[Node]) -> bool: routine_name = parent_routine.name if parent_routine else "" if routine_name not in PROFILING_IGNORE: MaximalProfilingOutsideDirectivesTrans().apply(children) + + +def iom_put_argument_to_temporary(calls: list[Call]): + '''Extracts the second argument of all iom_put calls and puts them + in a temporary if they are an Operation with an array datatype.''' + for call in calls: + if call.symbol.name == "iom_put": + arg = call.arguments[1] + dtype = arg.datatype + if isinstance(dtype, ArrayType) and isinstance(arg, Operation): + DataNodeToTempTrans().apply(arg) From dae4e86e12acec388fe5c61c0bc7d6b740a0e7e5 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Fri, 13 Mar 2026 14:21:01 +0000 Subject: [PATCH 02/25] transformation can't always work so catch exception --- examples/nemo/scripts/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/nemo/scripts/utils.py b/examples/nemo/scripts/utils.py index eb14d73965..1fde13d503 100755 --- a/examples/nemo/scripts/utils.py +++ b/examples/nemo/scripts/utils.py @@ -537,4 +537,7 @@ def iom_put_argument_to_temporary(calls: list[Call]): arg = call.arguments[1] dtype = arg.datatype if isinstance(dtype, ArrayType) and isinstance(arg, Operation): - DataNodeToTempTrans().apply(arg) + try: + DataNodeToTempTrans().apply(arg) + except TransformationError: + pass From a95ece0cef753944ba1443c223a82def50f61d5c Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Mon, 16 Mar 2026 13:16:54 +0000 Subject: [PATCH 03/25] fix to datanode to temp tarns to handle case sensitivity correctly via lookup --- .../transformations/datanode_to_temp_trans.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index 21198c1a77..b782b96658 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -147,9 +147,11 @@ def validate(self, node: DataNode, **kwargs): symbols.update(element.upper.get_all_accessed_symbols()) # Compare the symbols in the array bounds with the symbols # already in the scope. - scope_symbols = node.scope.symbol_table.get_symbols() + scope_table = node.scope.symbol_table for sym in symbols: - scoped_name_sym = scope_symbols.get(sym.name, None) + scoped_name_sym = scope_table.lookup( + sym.name, otherwise=None + ) # If sym is not scoped_name_sym, then there is a # symbol collision from an imported symbol. if scoped_name_sym and sym is not scoped_name_sym: @@ -176,9 +178,9 @@ def validate(self, node: DataNode, **kwargs): # If its an imported symbol we need to check if its # the same import interface. if isinstance(sym.interface, ImportInterface): - scoped_name_sym = scope_symbols.get( - sym.interface.container_symbol.name, - None + scoped_name_sym = scope_table.lookup( + sym.interface.container_symbol.name, + otherwise=None ) if scoped_name_sym and not isinstance( scoped_name_sym, ContainerSymbol): @@ -248,18 +250,20 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): symbols.update(element.lower.get_all_accessed_symbols()) if isinstance(element.upper, DataNode): symbols.update(element.upper.get_all_accessed_symbols()) - scope_symbols = node.scope.symbol_table.get_symbols() + scope_table = node.scope.symbol_table for sym in symbols: - scoped_name_sym = scope_symbols.get(sym.name, None) + scoped_name_sym = scope_table.lookup( + sym.name, otherwise=None + ) # If no symbol with the name exists then create one. if not scoped_name_sym: sym_copy = sym.copy() if isinstance(sym_copy.interface, ImportInterface): # Check if the ContainerSymbol is already in the # interface - container = scope_symbols.get( + container = scope_table.lookup( sym_copy.interface.container_symbol.name, - None + otherwise=None ) if container is None: # Add the container symbol to the symbol table From 6ce8a38663f6e198a688d58cedae48b83af8759e Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Tue, 17 Mar 2026 13:37:42 +0000 Subject: [PATCH 04/25] Try extending the iom_put transformation --- examples/nemo/scripts/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/nemo/scripts/utils.py b/examples/nemo/scripts/utils.py index 1fde13d503..f8f8bb9366 100755 --- a/examples/nemo/scripts/utils.py +++ b/examples/nemo/scripts/utils.py @@ -533,11 +533,13 @@ def iom_put_argument_to_temporary(calls: list[Call]): '''Extracts the second argument of all iom_put calls and puts them in a temporary if they are an Operation with an array datatype.''' for call in calls: - if call.symbol.name == "iom_put": - arg = call.arguments[1] + for arg in call.arguments: dtype = arg.datatype if isinstance(dtype, ArrayType) and isinstance(arg, Operation): try: DataNodeToTempTrans().apply(arg) - except TransformationError: - pass + except TransformationError as err: + call.append_preceding_comment( + f"Couldn't pull the second argument to a temporary " + f"due to the following error: {str(err.value)}" + ) From dd3e62aa39cd44ccf8c158923a86a5e86611fe0f Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Tue, 17 Mar 2026 15:09:29 +0000 Subject: [PATCH 05/25] fix --- .../psyir/transformations/datanode_to_temp_trans.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index b782b96658..2d6fd6d56a 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -46,6 +46,7 @@ Range, Reference, Statement, + Schedule, ) from psyclone.psyir.symbols.datatypes import ( ArrayType, @@ -306,8 +307,10 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): # Find the parent and position of the statement containing the # DataNode. - parent = node.ancestor(Statement).parent - pos = node.ancestor(Statement).position + schedule = node.ancestor(Schedule) + path = node.path_from(schedule) + pos = path[0] + parent = schedule.children[pos] # Replace the datanode with the new reference node.replace_with(new_ref) From e06176bcce88981dc2bcfb84061268f81b2459ce Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Tue, 17 Mar 2026 15:12:49 +0000 Subject: [PATCH 06/25] Fix datanodetotemptrans for ifblock statements and similar --- .../psyir/transformations/datanode_to_temp_trans.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index 2d6fd6d56a..40c2609567 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -305,12 +305,11 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): # Create a Reference to the new symbol new_ref = Reference(symbol) - # Find the parent and position of the statement containing the - # DataNode. + # Find the containing schedule and position of the statement + # containing the DataNode. schedule = node.ancestor(Schedule) path = node.path_from(schedule) pos = path[0] - parent = schedule.children[pos] # Replace the datanode with the new reference node.replace_with(new_ref) @@ -319,7 +318,7 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): assign = Assignment.create(new_ref.copy(), node) # Add the assignment into the tree. - parent.addchild(assign, pos) + schedule.addchild(assign, pos) # If the datatype is an array, we need to allocate the array # before the statement too. @@ -338,7 +337,7 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): ) # Add the allocate statement into the tree immediately before # its use. - parent.addchild(intrinsic, pos) + schedule.addchild(intrinsic, pos) __all__ = ["DataNodeToTempTrans"] From ed693617ece56e4c606dc3545b57766cafad34c3 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Tue, 17 Mar 2026 15:51:58 +0000 Subject: [PATCH 07/25] Use elemental_type in intrinsic_call --- src/psyclone/psyir/nodes/intrinsic_call.py | 54 ++++++++++++++-------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 9bfadbc9b5..f38c5ab9d6 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -212,14 +212,14 @@ def _type_of_named_arg_with_optional_kind_and_dim( arg = node.argument_by_name(arg_name) if "kind" in node.argument_names: dtype = ScalarType( - arg.datatype.intrinsic, + arg.datatype.elemental_type.intrinsic, node.argument_by_name("kind").copy(), ) else: # PSyclone has the UNDEFINED Precision as the default kind for all # supported inbuilt datatypes. dtype = ScalarType( - arg.datatype.intrinsic, + arg.datatype.elemental_type.intrinsic, ScalarType.Precision.UNDEFINED, ) # If "dim" argument isn't present then the result is an array of the same @@ -368,7 +368,7 @@ def _findloc_return_type(node: IntrinsicCall) -> DataType: """ if "kind" in node.argument_names: dtype = ScalarType( - node.argument_by_name("array").datatype.intrinsic, + node.argument_by_name("array").datatype.elemental_type.intrinsic, node.argument_by_name("kind").copy(), ) else: @@ -439,8 +439,8 @@ def _iparity_return_type(node: IntrinsicCall) -> DataType: :returns: the computed datatype for the IntrinsicCall. """ dtype = ScalarType( - node.argument_by_name("array").datatype.intrinsic, - node.argument_by_name("array").datatype.precision, + node.argument_by_name("array").datatype.elemental_type.intrinsic, + node.argument_by_name("array").datatype.elemental_type.precision, ) # If dim is not present then we return a scalar. if "dim" not in node.argument_names: @@ -564,8 +564,10 @@ def _maxval_return_type(node: IntrinsicCall) -> DataType: :returns: the computed datatype for the IntrinsicCall. """ - dtype = ScalarType(node.argument_by_name("array").datatype.intrinsic, - node.argument_by_name("array").datatype.precision) + dtype = ScalarType( + node.argument_by_name("array").datatype.elemental_type.intrinsic, + node.argument_by_name("array").datatype.elemental_type.precision + ) if "dim" not in node.argument_names: return dtype # We have a dimension specified. We don't know the resultant shape @@ -588,8 +590,8 @@ def _dot_product_return_type(node: IntrinsicCall) -> DataType: from psyclone.psyir.tools.type_info_computation import ( compute_scalar_type ) - veca_datatype = node.argument_by_name("vector_a").datatype - vecb_datatype = node.argument_by_name("vector_b").datatype + veca_datatype = node.argument_by_name("vector_a").datatype.elemental_type + vecb_datatype = node.argument_by_name("vector_b").datatype.elemental_type return compute_scalar_type( [ScalarType( veca_datatype.intrinsic, veca_datatype.precision @@ -3213,7 +3215,9 @@ class Intrinsic(IAttr, Enum): optional_args={"kind": DataNode}, return_type=lambda node: ( _type_of_scalar_with_optional_kind( - node, node.argument_by_name("l").datatype.intrinsic, + node, + node.argument_by_name("l"). + datatype.elemental_type.intrinsic, "kind", ) if "kind" in node.argument_names else _type_of_named_argument(node, "l") @@ -3761,8 +3765,10 @@ class Intrinsic(IAttr, Enum): optional_args={"vector": DataNode}, return_type=lambda node: ArrayType( ScalarType( - node.argument_by_name("array").datatype.intrinsic, - node.argument_by_name("array").datatype.precision), + node.argument_by_name("array").datatype. + elemental_type.intrinsic, + node.argument_by_name("array").datatype. + elemental_type.precision), [ArrayType.Extent.DEFERRED] ), reference_accesses=lambda node: ( @@ -3886,7 +3892,8 @@ class Intrinsic(IAttr, Enum): lambda node: _type_with_specified_precision_and_optional_dim( node, "array", - node.argument_by_name("array").datatype.intrinsic + node.argument_by_name("array").datatype. + elemental_type.intrinsic ) ), reference_accesses=lambda node: ( @@ -4443,8 +4450,10 @@ class Intrinsic(IAttr, Enum): optional_args={}, return_type=lambda node: ArrayType( ScalarType( - node.argument_by_name("source").datatype.intrinsic, - node.argument_by_name("source").datatype.precision), + node.argument_by_name("source").datatype. + elemental_type.intrinsic, + node.argument_by_name("source").datatype. + elemental_type.precision), ([ArrayType.Extent.DEFERRED] * (len(node.argument_by_name("source").datatype.shape) + 1) if isinstance(node.argument_by_name("source").datatype, @@ -4543,7 +4552,8 @@ class Intrinsic(IAttr, Enum): lambda node: _type_with_specified_precision_and_optional_dim( node, "array", - node.argument_by_name("array").datatype.intrinsic + node.argument_by_name("array").datatype. + elemental_type.intrinsic ) ), reference_accesses=lambda node: ( @@ -4714,8 +4724,10 @@ class Intrinsic(IAttr, Enum): ArrayType)) else ArrayType( ScalarType( - node.argument_by_name("mold").datatype.intrinsic, - node.argument_by_name("mold").datatype.precision + node.argument_by_name("mold").datatype. + elemental_type.intrinsic, + node.argument_by_name("mold").datatype. + elemental_type.precision ), [ArrayType.Extent.DEFERRED]) ), @@ -4737,8 +4749,10 @@ class Intrinsic(IAttr, Enum): arg_names=(("matrix",),)), optional_args={}, return_type=lambda node: ArrayType(ScalarType( - node.argument_by_name("matrix").datatype.intrinsic, - node.argument_by_name("matrix").datatype.precision), + node.argument_by_name("matrix").datatype. + elemental_type.intrinsic, + node.argument_by_name("matrix").datatype. + elemental_type.precision), [node.argument_by_name("matrix").datatype.shape[1], node.argument_by_name("matrix").datatype.shape[0]] ), From a912e881b1dd7011fe90937df4483dbbf35354d4 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Thu, 2 Apr 2026 10:58:29 +0100 Subject: [PATCH 08/25] Fixed the TypeError branch --- src/psyclone/psyir/nodes/intrinsic_call.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 67f56c6b19..abcfe8a163 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -4912,7 +4912,12 @@ def datatype(self) -> DataType: try: return self.intrinsic.return_type(self) except TypeError as err: - # FIXME This should be only sometimes? + # If we get an invalid argument to a ScalarType constructor it + # means we attempted to pass either an UnresolvedType into the + # datatype + if ("ScalarType expected 'intrinsic' argument to be of type " + "ScalarType.Intrinsic but found " in str(err)): + return UnresolvedType() # For array of structure or something. # return UnresolvedType() raise InternalError( From 78127dad3f0db66ecb7ebecb3bf84be3c79fd68d Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Thu, 2 Apr 2026 10:59:26 +0100 Subject: [PATCH 09/25] Changed the InternalError to be a fallthrough --- src/psyclone/psyir/nodes/intrinsic_call.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index abcfe8a163..0ce17ad1f3 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -4918,13 +4918,6 @@ def datatype(self) -> DataType: if ("ScalarType expected 'intrinsic' argument to be of type " "ScalarType.Intrinsic but found " in str(err)): return UnresolvedType() - # For array of structure or something. - # return UnresolvedType() - raise InternalError( - f"Failed to compute the datatype of a " - f"'{self.intrinsic.name}' intrinsic. This is likely due " - f"to not fully initialising the intrinsic correctly." - ) from err except AttributeError as err: # This is to handle when we call .intrinsic or # .precision on an UnresolvedType @@ -4938,13 +4931,14 @@ def datatype(self) -> DataType: and "NoneType" not in str(err)): return UnresolvedType() - # Can't use debug string due to this being a potentially - # incomplete IntrinsicCall - raise InternalError( - f"Failed to compute the datatype of a " - f"'{self.intrinsic.name}' intrinsic. This is likely due " - f"to not fully initialising the intrinsic correctly." - ) from err + # Fall through to the internalerror. + # Can't use debug string due to this being a potentially + # incomplete IntrinsicCall + raise InternalError( + f"Failed to compute the datatype of a " + f"'{self.intrinsic.name}' intrinsic. This is likely due " + f"to not fully initialising the intrinsic correctly." + ) from err else: return self.intrinsic.return_type From c3804eb52531ec80d455e398125da3f3f60f96d1 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Thu, 2 Apr 2026 11:01:32 +0100 Subject: [PATCH 10/25] Store the error to fallthrough to internal error correctly --- src/psyclone/psyir/nodes/intrinsic_call.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 0ce17ad1f3..77ac24ae6f 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -4918,6 +4918,7 @@ def datatype(self) -> DataType: if ("ScalarType expected 'intrinsic' argument to be of type " "ScalarType.Intrinsic but found " in str(err)): return UnresolvedType() + outerr = err except AttributeError as err: # This is to handle when we call .intrinsic or # .precision on an UnresolvedType @@ -4931,6 +4932,7 @@ def datatype(self) -> DataType: and "NoneType" not in str(err)): return UnresolvedType() + outerr = err # Fall through to the internalerror. # Can't use debug string due to this being a potentially # incomplete IntrinsicCall @@ -4938,7 +4940,7 @@ def datatype(self) -> DataType: f"Failed to compute the datatype of a " f"'{self.intrinsic.name}' intrinsic. This is likely due " f"to not fully initialising the intrinsic correctly." - ) from err + ) from outerr else: return self.intrinsic.return_type From 0420aa42c9938a508cd341d541cf503ce421ad1c Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Thu, 2 Apr 2026 14:19:46 +0100 Subject: [PATCH 11/25] precision handling for the TypeError --- src/psyclone/psyir/nodes/intrinsic_call.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 77ac24ae6f..1ef96ab97d 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -4916,8 +4916,10 @@ def datatype(self) -> DataType: # means we attempted to pass either an UnresolvedType into the # datatype if ("ScalarType expected 'intrinsic' argument to be of type " - "ScalarType.Intrinsic but found " in str(err)): + or "ScalarType expected 'precision' argument to be of " + "type " in str(err)): return UnresolvedType() + # FIXME Is this reachable? Monkeypatch probably. outerr = err except AttributeError as err: # This is to handle when we call .intrinsic or From 470ed98d918915df9732aae07bd05f27e6352863 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Tue, 7 Apr 2026 12:08:25 +0100 Subject: [PATCH 12/25] Added an if allocated test and check that we don't block potential loop parallelisation wherever possible.w --- .../transformations/datanode_to_temp_trans.py | 42 +++++++++++++++++-- .../datanode_to_temp_trans_test.py | 27 ++++++++---- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index 40c2609567..22b44cccc1 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -42,11 +42,14 @@ Assignment, Call, DataNode, + IfBlock, IntrinsicCall, + Loop, Range, Reference, Statement, Schedule, + UnaryOperation, ) from psyclone.psyir.symbols.datatypes import ( ArrayType, @@ -59,6 +62,7 @@ ) from psyclone.psyir.symbols import ( DataSymbol, ImportInterface, ContainerSymbol, Symbol) +from psyclone.psyir.tools.dependency_tools import DependencyTools from psyclone.utils import transformation_documentation_wrapper @@ -321,7 +325,7 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): schedule.addchild(assign, pos) # If the datatype is an array, we need to allocate the array - # before the statement too. + # before the statement too if its not already allocated. if isinstance(datatype, ArrayType): # Create an array reference to the symbol with the dimensions # returned by the datatype call earlier. @@ -335,9 +339,39 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): IntrinsicCall.Intrinsic.ALLOCATE, (ref,) ) - # Add the allocate statement into the tree immediately before - # its use. - schedule.addchild(intrinsic, pos) + allocated = IntrinsicCall.create( + IntrinsicCall.Intrinsic.ALLOCATED, + (Reference(symbol),) + ) + + ifblock = IfBlock.create( + UnaryOperation.create( + UnaryOperation.Operator.NOT, + allocated), + [intrinsic] + ) + # Check if there's ancestor loops. + loop = node.ancestor(Loop) + last_loop = None + if loop: + dtools = DependencyTools() + # while the ancestor can be parallelised, we should search + # higher for where to place the allocate. + while loop: + if dtools.can_loop_be_parallelised(loop): + last_loop = loop + loop = loop.ancestor(Loop) + else: + loop = None + # After this, the last loop that can be parallelised + # is set to last_loop. If last_loop is None we can + # use the previously computed schedule and position + if last_loop: + schedule = last_loop.ancestor(Schedule) + pos = last_loop.position + # Add the allocate statement and the containing ifblock into the + # tree immediately before its use. + schedule.addchild(ifblock, pos) __all__ = ["DataNodeToTempTrans"] diff --git a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py index f362b7c4ec..2b3596daad 100644 --- a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py @@ -287,7 +287,9 @@ def test_datanodetotemptrans_apply(fortran_reader, fortran_writer, tmp_path, out = fortran_writer(psyir) assert """ integer, allocatable, dimension(:,:) :: tmp - ALLOCATE(tmp(1:SIZE(a, dim=1),1:SIZE(b, dim=2))) + if (.NOT.ALLOCATED(tmp)) then + ALLOCATE(tmp(1:SIZE(a, dim=1),1:SIZE(b, dim=2))) + end if tmp = MATMUL(a, b) d = c + tmp""" in out assert Compile(tmp_path).string_compiles(out) @@ -338,7 +340,9 @@ def test_datanodetotemptrans_apply(fortran_reader, fortran_writer, tmp_path, out = fortran_writer(psyir) assert """ integer, allocatable, dimension(:) :: tmp - ALLOCATE(tmp(1:3)) + if (.NOT.ALLOCATED(tmp)) then + ALLOCATE(tmp(1:3)) + end if tmp = 3 * b a(:4) = tmp""" in out assert Compile(tmp_path).string_compiles(out) @@ -397,7 +401,9 @@ def test_datanodetotemptrans_apply_imports( out = fortran_writer(psyir) assert """ integer, allocatable, dimension(:,:) :: tmp - ALLOCATE(tmp(1:25,1:50)) + if (.NOT.ALLOCATED(tmp)) then + ALLOCATE(tmp(1:25,1:50)) + end if tmp = some_var b = tmp""" in out @@ -424,7 +430,9 @@ def test_datanodetotemptrans_apply_imports( integer, dimension(25,50) :: b integer, allocatable, dimension(:,:) :: tmp - ALLOCATE(tmp(1:25,1:i)) + if (.NOT.ALLOCATED(tmp)) then + ALLOCATE(tmp(1:25,1:i)) + end if tmp = some_var b = tmp""" in out @@ -462,7 +470,9 @@ def test_datanodetotemptrans_apply_imports( use f_mod, only : some_var integer, allocatable, dimension(:,:) :: tmp - ALLOCATE(tmp(1:25,1:i)) + if (.NOT.ALLOCATED(tmp)) then + ALLOCATE(tmp(1:25,1:i)) + end if tmp = some_var j = tmp""" in out @@ -493,6 +503,9 @@ def test_datanodetotemptrans_apply_nemo_example(fortran_reader, out = fortran_writer(psyir) assert """real, allocatable, dimension(:,:,:) :: tmp - ALLOCATE(tmp(1:nie0 - nis0 + 1,1:nje0 - njs0 + 1,1:SIZE(rn2, dim=3))) + if (.NOT.ALLOCATED(tmp)) then + ALLOCATE(tmp(1:nie0 - nis0 + 1,1:nje0 - njs0 + 1,1:SIZE(rn2, dim=3))) + end if tmp = -avt_k(:,:,:) * rn2(nis0:nie0,njs0:nje0,:) * \ -wmask(nis0:nie0,njs0:nje0,:)""" in out +wmask(nis0:nie0,njs0:nje0,:) + call iom_put('estrat_k', tmp)""" in out From cf4947ecd097b21235c5cf7e9e6effa4d69b597c Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Tue, 7 Apr 2026 14:15:24 +0100 Subject: [PATCH 13/25] [skip-ci] Some changes to add test (that fails) for the datanode_to_temptrans --- .../datanode_to_temp_trans_test.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py index 2b3596daad..0bd702082f 100644 --- a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py @@ -40,7 +40,7 @@ from psyclone.configuration import Config from psyclone.psyir.frontend.fortran import FortranReader from psyclone.psyir.nodes import ( - Assignment, Reference + Assignment, IntrinsicCall, Reference ) from psyclone.psyir.symbols import ( DataSymbol, INTEGER_TYPE @@ -509,3 +509,29 @@ def test_datanodetotemptrans_apply_nemo_example(fortran_reader, tmp = -avt_k(:,:,:) * rn2(nis0:nie0,njs0:nje0,:) * \ wmask(nis0:nie0,njs0:nje0,:) call iom_put('estrat_k', tmp)""" in out + + +def test_datanodetotemptrans_apply_loops(fortran_reader, + fortran_writer): + '''Test that if we have parallelisable (and non-parallelisable) loops + around the extracted node we place the new code in the correct place.''' + code = """subroutine test + integer :: i + real, dimension(100) :: arr, arr2 + do i = 1, 100 + arr(i) = SUM(ABS(arr2)) + end do + end subroutine test""" + + psyir = fortran_reader.psyir_from_source(code) + dtrans = DataNodeToTempTrans() + from psyclone.psyir.nodes import Loop + loop = psyir.walk(Loop)[0] + from psyclone.psyir.tools.dependency_tools import DependencyTools + dtools = DependencyTools() + print(dtools.can_loop_be_parallelised(loop)) + # apply the extraction to the ABS(arr2) + dtrans.apply(psyir.walk(IntrinsicCall)[1]) + out = fortran_writer(psyir) + print(out) + assert False From c6c718c13d06357abdbcb60bc0ca056b0e34b85d Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Tue, 7 Apr 2026 15:36:57 +0100 Subject: [PATCH 14/25] Don't try to move the allocate statement which wasn't very feasible for now --- .../transformations/datanode_to_temp_trans.py | 21 -------------- .../datanode_to_temp_trans_test.py | 28 +------------------ 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index 22b44cccc1..c804fced6f 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -44,7 +44,6 @@ DataNode, IfBlock, IntrinsicCall, - Loop, Range, Reference, Statement, @@ -62,7 +61,6 @@ ) from psyclone.psyir.symbols import ( DataSymbol, ImportInterface, ContainerSymbol, Symbol) -from psyclone.psyir.tools.dependency_tools import DependencyTools from psyclone.utils import transformation_documentation_wrapper @@ -350,25 +348,6 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): allocated), [intrinsic] ) - # Check if there's ancestor loops. - loop = node.ancestor(Loop) - last_loop = None - if loop: - dtools = DependencyTools() - # while the ancestor can be parallelised, we should search - # higher for where to place the allocate. - while loop: - if dtools.can_loop_be_parallelised(loop): - last_loop = loop - loop = loop.ancestor(Loop) - else: - loop = None - # After this, the last loop that can be parallelised - # is set to last_loop. If last_loop is None we can - # use the previously computed schedule and position - if last_loop: - schedule = last_loop.ancestor(Schedule) - pos = last_loop.position # Add the allocate statement and the containing ifblock into the # tree immediately before its use. schedule.addchild(ifblock, pos) diff --git a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py index 0bd702082f..2b3596daad 100644 --- a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py @@ -40,7 +40,7 @@ from psyclone.configuration import Config from psyclone.psyir.frontend.fortran import FortranReader from psyclone.psyir.nodes import ( - Assignment, IntrinsicCall, Reference + Assignment, Reference ) from psyclone.psyir.symbols import ( DataSymbol, INTEGER_TYPE @@ -509,29 +509,3 @@ def test_datanodetotemptrans_apply_nemo_example(fortran_reader, tmp = -avt_k(:,:,:) * rn2(nis0:nie0,njs0:nje0,:) * \ wmask(nis0:nie0,njs0:nje0,:) call iom_put('estrat_k', tmp)""" in out - - -def test_datanodetotemptrans_apply_loops(fortran_reader, - fortran_writer): - '''Test that if we have parallelisable (and non-parallelisable) loops - around the extracted node we place the new code in the correct place.''' - code = """subroutine test - integer :: i - real, dimension(100) :: arr, arr2 - do i = 1, 100 - arr(i) = SUM(ABS(arr2)) - end do - end subroutine test""" - - psyir = fortran_reader.psyir_from_source(code) - dtrans = DataNodeToTempTrans() - from psyclone.psyir.nodes import Loop - loop = psyir.walk(Loop)[0] - from psyclone.psyir.tools.dependency_tools import DependencyTools - dtools = DependencyTools() - print(dtools.can_loop_be_parallelised(loop)) - # apply the extraction to the ABS(arr2) - dtrans.apply(psyir.walk(IntrinsicCall)[1]) - out = fortran_writer(psyir) - print(out) - assert False From 03e0ffa487a5fd2efd01bafef44169709b8d765b Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Wed, 8 Apr 2026 14:57:41 +0100 Subject: [PATCH 15/25] Hoist the allocate statement if we think its safe --- .../transformations/datanode_to_temp_trans.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index c804fced6f..8c28763937 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -44,6 +44,7 @@ DataNode, IfBlock, IntrinsicCall, + Loop, Range, Reference, Statement, @@ -348,6 +349,26 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): allocated), [intrinsic] ) + # If the shape doesn't contain array references then we can hoist + # the allocate statement outside of any ancestor loops. + hoistable = True + for shape in ref.indices: + for ref2 in shape.walk(Reference): + if isinstance(ref2, ArrayReference): + hoistable = False + # If we can hoist the allocate, find the highest level Loop + # ancestor and set the schedule and position to place the + # allocate before this loop. + if hoistable: + loop_anc = schedule.ancestor(Loop) + finger = loop_anc + while finger: + loop_anc = finger + finger = finger.ancestor(Loop) + if loop_anc: + pos = loop_anc.position + schedule = loop_anc.ancestor(Schedule) + # Add the allocate statement and the containing ifblock into the # tree immediately before its use. schedule.addchild(ifblock, pos) From f147e9499b27f0ff940cfd8149c51b6cee248e7c Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Wed, 8 Apr 2026 15:14:24 +0100 Subject: [PATCH 16/25] updated script error --- examples/nemo/scripts/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/nemo/scripts/utils.py b/examples/nemo/scripts/utils.py index 7c663e2d0f..ea841520bd 100755 --- a/examples/nemo/scripts/utils.py +++ b/examples/nemo/scripts/utils.py @@ -537,6 +537,6 @@ def iom_put_argument_to_temporary(calls: list[Call]): DataNodeToTempTrans().apply(arg) except TransformationError as err: call.append_preceding_comment( - f"Couldn't pull the second argument to a temporary " + f"Couldn't pull the argument {arg} to a temporary " f"due to the following error: {str(err.value)}" ) From 339f8b43c75885a547d1c3fa8c0cebaf3f6e6fd9 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Thu, 16 Apr 2026 15:47:17 +0100 Subject: [PATCH 17/25] Revert to only apply to iom_put --- examples/nemo/scripts/utils.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/examples/nemo/scripts/utils.py b/examples/nemo/scripts/utils.py index ea841520bd..b552c464df 100755 --- a/examples/nemo/scripts/utils.py +++ b/examples/nemo/scripts/utils.py @@ -530,13 +530,16 @@ def iom_put_argument_to_temporary(calls: list[Call]): '''Extracts the second argument of all iom_put calls and puts them in a temporary if they are an Operation with an array datatype.''' for call in calls: - for arg in call.arguments: - dtype = arg.datatype - if isinstance(dtype, ArrayType) and isinstance(arg, Operation): - try: - DataNodeToTempTrans().apply(arg) - except TransformationError as err: - call.append_preceding_comment( - f"Couldn't pull the argument {arg} to a temporary " - f"due to the following error: {str(err.value)}" - ) + if call.symbol.name == "iom_put": + for arg in call.arguments: + dtype = arg.datatype + if (isinstance(dtype, ArrayType) and + isinstance(arg, Operation)): + try: + DataNodeToTempTrans().apply(arg) + except TransformationError as err: + call.append_preceding_comment( + f"Couldn't pull the argument {arg} to a " + f"temporary due to the following error: " + f"{str(err.value)}" + ) From 4fb5d7b7af09f916c9ad95a3c16e9e9a15be45c0 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Tue, 21 Apr 2026 14:37:24 +0100 Subject: [PATCH 18/25] Remaining test coverage and fixed a missing fstring in fparser2 frontend --- src/psyclone/psyir/frontend/fparser2.py | 4 +- src/psyclone/psyir/nodes/intrinsic_call.py | 4 +- .../tests/psyir/nodes/intrinsic_call_test.py | 77 +++++++++++++++++++ .../datanode_to_temp_trans_test.py | 61 +++++++++++++++ 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index a7d2123cab..ef2154260a 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1112,8 +1112,8 @@ def _fparser2_tree_from_fparser2_reader( except (FortranSyntaxError, NoMatchError) as err: raise ValueError( f"Failed to parse the provided source code:\n{source_code}" - "\nError was: {err}\nIs the input valid Fortran (note that" - f" CPP directives must be handled by a pre-processor)?" + f"\nError was: {err}\nIs the input valid Fortran (note " + f"that CPP directives must be handled by a pre-processor)?" ) from err try: # If it reaches this point a partial_code was provided, attempt diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 8140a178aa..ca4edb5496 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -4918,10 +4918,12 @@ def datatype(self) -> DataType: # means we attempted to pass either an UnresolvedType into the # datatype if ("ScalarType expected 'intrinsic' argument to be of type " + in str(err) or "ScalarType expected 'precision' argument to be of " "type " in str(err)): return UnresolvedType() - # FIXME Is this reachable? Monkeypatch probably. + # Is this reachable? Tested via monkeypatch as there may be + # some edge case I can't think of. outerr = err except AttributeError as err: # This is to handle when we call .intrinsic or diff --git a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py index 8e5a4f232f..d765ee5d63 100644 --- a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py +++ b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py @@ -43,6 +43,7 @@ """ import pytest +from enum import Enum from psyclone.core.access_type import AccessType from psyclone.errors import InternalError @@ -51,11 +52,13 @@ Assignment, BinaryOperation, Call, + DataNode, Literal, Schedule, Reference, ) from psyclone.psyir.nodes.intrinsic_call import ( + ArgDesc, IntrinsicCall, IAttr, _type_of_arg_with_rank_minus_one, @@ -175,6 +178,80 @@ def test_intrinsiccall_datatype(fortran_reader): assert isinstance(call.datatype, UnresolvedType) +def test_intrinsiccall_datatype_typeerr_paths( + fortran_reader, monkeypatch +): + '''Test the TypeError raising paths inside the datatype function + of IntrinsicCall.''' + + def expected_typeerr(*args, **kwargs): + '''Raise one of the expected type errors that datatype can handle.''' + raise TypeError( + "ScalarType expected 'intrinsic' argument to be of type " + ) + + def unexpected_typeerr(*args, **kwargs): + '''Raise some other type error that datatype raises an error from.''' + raise TypeError( + "This is a bad type error." + ) + + class Intrinsic(IAttr, Enum): + '''Test class to override the base Intrinsic enum''' + MAX = IAttr( + name="MAX", + is_pure=True, + is_elemental=True, + is_inquiry=False, + # No upper limit on argument type so we don't store an + # argument list of names. + required_args=ArgDesc( + min_count=2, + max_count=None, + types=DataNode, + arg_names=((None,),)), + optional_args={}, + return_type=expected_typeerr, + reference_accesses=None, + ) + MIN = IAttr( + name="MIN", + is_pure=True, + is_elemental=True, + is_inquiry=False, + # No upper limit on argument type so we don't store an + # argument list of names. + required_args=ArgDesc( + min_count=2, + max_count=None, + types=DataNode, + arg_names=((None,),)), + optional_args={}, + return_type=unexpected_typeerr, + reference_accesses=None, + ) + + # Replace the Intrinsic Enum as its not modifiable. + monkeypatch.setattr(IntrinsicCall, "Intrinsic", Intrinsic) + + code = """subroutine test + integer, dimension(100) :: i + integer :: j + + j = MAX(i(1), i(2)) + j = MIN(i(3), i(4)) + end subroutine test""" + psyir = fortran_reader.psyir_from_source(code) + intrinsics = psyir.walk(IntrinsicCall) + + assert isinstance(intrinsics[0].datatype, UnresolvedType) + with pytest.raises(InternalError) as err: + _ = intrinsics[1].datatype + assert ("Failed to compute the datatype of a 'MIN' intrinsic. " + "This is likely due to not fully initialising the intrinsic " + "correctly." in str(err.value)) + + def test_intrinsiccall_reference_accesses_no_arg_names(): """Test the case of IntrinsicCall's reference_accesses method where the call to compute_argument_names fails.""" diff --git a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py index b5224deef9..72c55e3416 100644 --- a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py @@ -509,3 +509,64 @@ def test_datanodetotemptrans_apply_nemo_example(fortran_reader, tmp = -avt_k(:,:,:) * rn2(nis0:nie0,njs0:nje0,:) * \ wmask(nis0:nie0,njs0:nje0,:) call iom_put('estrat_k', tmp)""" in out + + +def test_datanodetotemptrans_hoistable_array(fortran_reader, + fortran_writer): + ''' + Takes an array sized datanode and ensures the allocate is hoisted out of + the containing loop if possible. + ''' + + code = """subroutine test + use some_mod, only: some_func + integer :: i + real, dimension(100,100) :: arr1, arr2 + + do i = 1, 100 + call some_func(arr1*arr2) + end do + end subroutine test""" + + psyir = fortran_reader.psyir_from_source(code) + dtrans = DataNodeToTempTrans() + dtrans.apply( + psyir.children[0].children[0].loop_body.children[0].arguments[0] + ) + + out = fortran_writer(psyir) + assert """ if (.NOT.ALLOCATED(tmp)) then + ALLOCATE(tmp(1:100,1:100)) + end if + do i = 1, 100, 1 + tmp = arr1 * arr2 + call some_func(tmp) + enddo""" in out + + # If the shape of the result contains an array expression then we + # shouldn't hoist. + + code = """subroutine test + use some_mod, only: some_func + integer :: i + real, dimension(100, 100) :: arr1, arr2 + integer, dimension(100) :: arr3 + + do i = 1, 100 + call some_func(arr1(1:arr3(i),:)*arr2(1:arr3(i),:)) + end do + end subroutine test""" + psyir = fortran_reader.psyir_from_source(code) + dtrans = DataNodeToTempTrans() + dtrans.apply( + psyir.children[0].children[0].loop_body.children[0].arguments[0] + ) + + out = fortran_writer(psyir) + assert """ do i = 1, 100, 1 + if (.NOT.ALLOCATED(tmp)) then + ALLOCATE(tmp(1:arr3(i),1:100)) + end if + tmp = arr1(:arr3(i),:) * arr2(:arr3(i),:) + call some_func(tmp) + enddo""" in out From fa35a5800293406ad4b25faca75302c3a2af3354 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Thu, 23 Apr 2026 17:21:28 +0100 Subject: [PATCH 19/25] Some changes towards review --- examples/nemo/scripts/omp_cpu_trans.py | 10 +-- examples/nemo/scripts/omp_gpu_trans.py | 10 +-- examples/nemo/scripts/utils.py | 11 ++- src/psyclone/psyir/nodes/intrinsic_call.py | 73 ++++--------------- .../transformations/datanode_to_temp_trans.py | 59 ++++++++++++--- .../tests/psyir/nodes/intrinsic_call_test.py | 73 ------------------- .../datanode_to_temp_trans_test.py | 35 ++++++--- 7 files changed, 104 insertions(+), 167 deletions(-) diff --git a/examples/nemo/scripts/omp_cpu_trans.py b/examples/nemo/scripts/omp_cpu_trans.py index 99526b73b5..773a78e48b 100755 --- a/examples/nemo/scripts/omp_cpu_trans.py +++ b/examples/nemo/scripts/omp_cpu_trans.py @@ -40,9 +40,8 @@ import os from utils import ( insert_explicit_loop_parallelism, normalise_loops, add_profiling, - iom_put_argument_to_temporary, PARALLELISATION_ISSUES, NEMO_MODULES_TO_IMPORT) -from psyclone.psyir.nodes import Routine, Call +from psyclone.psyir.nodes import Routine from psyclone.transformations import OMPLoopTrans # Enable the insertion of profiling hooks during the transformation script @@ -108,10 +107,6 @@ def trans(psyir): for subroutine in psyir.walk(Routine): print(f"Adding OpenMP threading to subroutine: {subroutine.name}") - # Extract any array operations from iom_put calls to temporary - # expressions that can be parallelised. - iom_put_argument_to_temporary(subroutine.walk(Call)) - if PROFILING_ENABLED: add_profiling(subroutine.children) @@ -122,7 +117,8 @@ def trans(psyir): loopify_array_intrinsics=True, convert_range_loops=True, hoist_expressions=False, - scalarise_loops=False + scalarise_loops=False, + hoist_argument_expressions=True ) if psyir.name not in PARALLELISATION_ISSUES: diff --git a/examples/nemo/scripts/omp_gpu_trans.py b/examples/nemo/scripts/omp_gpu_trans.py index 54282a4704..586ee1c95e 100755 --- a/examples/nemo/scripts/omp_gpu_trans.py +++ b/examples/nemo/scripts/omp_gpu_trans.py @@ -40,9 +40,8 @@ import os from utils import ( add_profiling, inline_calls, insert_explicit_loop_parallelism, - normalise_loops, iom_put_argument_to_temporary, - PARALLELISATION_ISSUES, NEMO_MODULES_TO_IMPORT) -from psyclone.psyir.nodes import Routine, Loop, Call + normalise_loops, PARALLELISATION_ISSUES, NEMO_MODULES_TO_IMPORT) +from psyclone.psyir.nodes import Routine, Loop from psyclone.psyir.transformations import ( OMPTargetTrans, OMPDeclareTargetTrans) from psyclone.transformations import ( @@ -202,8 +201,6 @@ def trans(psyir): # Extract any array operations from iom_put calls to temporary # expressions that can be parallelised. - iom_put_argument_to_temporary(subroutine.walk(Call)) - normalise_loops( subroutine, hoist_local_arrays=False, @@ -211,7 +208,8 @@ def trans(psyir): loopify_array_intrinsics=True, convert_range_loops=True, increase_array_ranks=not NEMOV4, - hoist_expressions=True + hoist_expressions=True, + hoist_argument_expressions=True, ) # Perform module-inlining of called routines. if INLINING_ENABLED: diff --git a/examples/nemo/scripts/utils.py b/examples/nemo/scripts/utils.py index 00cd39a041..72c608f707 100755 --- a/examples/nemo/scripts/utils.py +++ b/examples/nemo/scripts/utils.py @@ -181,6 +181,7 @@ def normalise_loops( scalarise_loops: bool = False, increase_array_ranks: bool = False, hoist_expressions: bool = True, + hoist_argument_expressions: bool = True, ): ''' Normalise all loops in the given schedule so that they are in an appropriate form for the Parallelisation transformations to analyse @@ -201,7 +202,14 @@ def normalise_loops( arrays. :param hoist_expressions: whether to hoist bounds and loop invariant statements out of the loop nest. + :param hoist_argument_expressions: whether to hoist array expressions + out of the containing Call. ''' + # TODO #3412: This is currently limited to iom_put, we want to expand it + # throughout the code + if hoist_argument_expressions: + iom_put_argument_to_temporary(schedule.walk(Call)) + if hoist_local_arrays and schedule.name not in CONTAINS_STMT_FUNCTIONS: # Apply the HoistLocalArraysTrans when possible, it cannot be applied # to files with statement functions because it will attempt to put the @@ -545,7 +553,8 @@ def iom_put_argument_to_temporary(calls: list[Call]): DataNodeToTempTrans().apply(arg) except TransformationError as err: call.append_preceding_comment( - f"Couldn't pull the argument {arg} to a " + f"PSyclone Warning: Couldn't pull the argument " + f"{arg.debug_string().rstrip()} to a " f"temporary due to the following error: " f"{str(err.value)}" ) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index aa545c0f2f..a098f6b76a 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -259,6 +259,7 @@ def _type_of_named_arg_accounting_for_dim_arg( not isinstance(arg_dt.elemental_type, ScalarType) or not isinstance(arg_dt.elemental_type.intrinsic, ScalarType.Intrinsic) ): + print("?", arg_dt, type(arg_dt), arg_dt.elemental_type) return UnresolvedType() dtype = arg_dt.elemental_type # If dim is not present, return the same datatype @@ -427,34 +428,6 @@ def _int_return_type(node: IntrinsicCall) -> DataType: ) -def _iparity_return_type(node: IntrinsicCall) -> DataType: - """Helper function for the IPARITY case. - - The result is the same type as the "array" argument. If the - "dim" argument is not present, a scalar of that type is returned. - Otherwise an ArrayType of rank n-1 (where n is the rank of "array") of - that type is returned instead. - - :param node: The IntrinsicCall whose return type to compute. - - :returns: the computed datatype for the IntrinsicCall. - """ - dtype = ScalarType( - node.argument_by_name("array").datatype.intrinsic, - node.argument_by_name("array").datatype.precision, - ) - # If dim is not present then we return a scalar. - if "dim" not in node.argument_names: - return dtype - # We have a dimension specified. We don't know the resultant shape - # in any detail as its dependent on the value of dim - return ArrayType( - dtype, - [ArrayType.Extent.DEFERRED] - * (len(node.argument_by_name("array").datatype.shape) - 1), - ) - - def _get_bound_function_return_type(node: IntrinsicCall) -> DataType: """Helper function for the return types of functions like LBOUND and LCOBOUND etc. @@ -552,31 +525,6 @@ def _matmul_return_type(node: IntrinsicCall) -> DataType: return ArrayType(stype, shape) -def _maxval_return_type(node: IntrinsicCall) -> DataType: - """ Helper function for the MAXVAL (and similar) intrinsic return - types. - - If the "dim" argument is absent, or the "array" argument has rank one - then the result is a ScalarType of the type of the "array" argument. - Otherwise the result is an ArrayType of rank n-1 (where n is the rank of - the "array" argument) with the same datatype of the "array" argument. - - :param node: The IntrinsicCall whose return type to compute. - - :returns: the computed datatype for the IntrinsicCall. - """ - dtype = ScalarType( - node.argument_by_name("array").datatype.intrinsic, - node.argument_by_name("array").datatype.precision - ) - arg = node.argument_by_name("array") - if "dim" not in node.argument_names: - return dtype - # We have a dimension specified. We don't know the resultant shape - # in any detail as its dependent on the value of dim - return _type_of_arg_with_rank_minus_one(arg, dtype) - - def _dot_product_return_type(node: IntrinsicCall) -> DataType: """Helper value for DOT_PRODUCT intrinsic return type. @@ -2854,7 +2802,9 @@ class Intrinsic(IAttr, Enum): ) ), optional_args={"mask": DataNode}, - return_type=_iparity_return_type, + return_type=lambda node: ( + _type_of_named_arg_accounting_for_dim_arg(node, "array") + ), reference_accesses=lambda node: ( _compute_reference_accesses( node @@ -3219,8 +3169,7 @@ class Intrinsic(IAttr, Enum): return_type=lambda node: ( _type_of_scalar_with_optional_kind( node, - node.argument_by_name("l"). - datatype.intrinsic, + node.argument_by_name("l").datatype.intrinsic, "kind", ) if "kind" in node.argument_names else _type_of_named_argument(node, "l") @@ -3378,7 +3327,9 @@ class Intrinsic(IAttr, Enum): ) ), optional_args={"mask": DataNode}, - return_type=_maxval_return_type, + return_type=lambda node: ( + _type_of_named_arg_accounting_for_dim_arg(node, "array") + ), reference_accesses=lambda node: ( _compute_reference_accesses( node @@ -3507,7 +3458,9 @@ class Intrinsic(IAttr, Enum): ) ), optional_args={"mask": DataNode}, - return_type=_maxval_return_type, + return_type=lambda node: ( + _type_of_named_arg_accounting_for_dim_arg(node, "array") + ), reference_accesses=lambda node: ( _compute_reference_accesses( node, @@ -4062,7 +4015,9 @@ class Intrinsic(IAttr, Enum): optional_args={"mask": DataNode, "identity": DataNode, "ordered": DataNode}, - return_type=_maxval_return_type, + return_type=lambda node: ( + _type_of_named_arg_accounting_for_dim_arg(node, "array") + ), reference_accesses=lambda node: ( _compute_reference_accesses( node diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index 8c28763937..30d7b1df7d 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -115,6 +115,7 @@ def validate(self, node: DataNode, **kwargs): """ # Validate the input options and types. self.validate_options(**kwargs) + verbose = self.get_option("verbose", **kwargs) if not isinstance(node, DataNode): raise TypeError( @@ -127,21 +128,31 @@ def validate(self, node: DataNode, **kwargs): calls = node.walk(Call) for call in calls: if not call.is_pure: - raise TransformationError( + message = ( f"Input node to {self.name} contains a call " f"'{call.debug_string().strip()}' that is not guaranteed " f"to be pure. Input node is " f"'{node.debug_string().strip()}'." ) + if verbose: + node.ancestor(Statement).append_preceding_comment( + f"PSyclone Warning: {message}" + ) + raise TransformationError(message) if isinstance(dtype, ArrayType): for element in dtype.shape: if element in [ArrayType.Extent.DEFERRED, ArrayType.Extent.ATTRIBUTE]: - raise TransformationError( + message = ( f"Input node's datatype is an array of unknown size, " f"so the {self.name} cannot be applied. " f"Input node was '{node.debug_string().strip()}'." ) + if verbose: + node.ancestor(Statement).append_preceding_comment( + f"PSyclone Warning: {message}" + ) + raise TransformationError(message) # The shape must now be set by ArrayBounds, we need to # examine the symbols used to define those bounds. symbols = set() @@ -163,22 +174,32 @@ def validate(self, node: DataNode, **kwargs): # container then we can skip this. if scoped_name_sym.interface == sym.interface: continue - raise TransformationError( + message = ( f"The type of the node supplied to {self.name} " f"depends upon an imported symbol '{sym.name}' " f"which has a name clash with a symbol in the " f"current scope." ) + if verbose: + node.ancestor(Statement).append_preceding_comment( + f"PSyclone Warning: {message}" + ) + raise TransformationError(message) # If its not in the current scope, and its visibility is # private then we can't import it. if (not scoped_name_sym and sym.visibility == Symbol.Visibility.PRIVATE): - raise TransformationError( + message = ( f"The datatype of the node suppled to " f"{self.name} depends upon an imported symbol " f"'{sym.name}' that is declared as private in " f"its containing module, so cannot be imported." ) + if verbose: + node.ancestor(Statement).append_preceding_comment( + f"PSyclone Warning: {message}" + ) + raise TransformationError(message) # If its an imported symbol we need to check if its # the same import interface. if isinstance(sym.interface, ImportInterface): @@ -188,13 +209,19 @@ def validate(self, node: DataNode, **kwargs): ) if scoped_name_sym and not isinstance( scoped_name_sym, ContainerSymbol): - raise TransformationError( + message = ( f"Input node contains an imported symbol " f"'{sym.name}' whose containing module " f"collides with an existing symbol. Colliding " f"name is " f"'{sym.interface.container_symbol.name}'." ) + if verbose: + node.ancestor(Statement).\ + append_preceding_comment( + f"PSyclone Warning: {message}" + ) + raise TransformationError(message) if node.ancestor(Statement) is None: raise TransformationError( @@ -223,18 +250,26 @@ def validate(self, node: DataNode, **kwargs): f"RESOLVE_IMPORTS in the transformation script may " f"enable resolution of these symbols." ) + if verbose: + node.ancestor(Statement).append_preceding_comment( + f"PSyclone Warning: {message}" + ) raise TransformationError(message) - def apply(self, node: DataNode, storage_name: str = "", **kwargs): + def apply(self, node: DataNode, storage_name: str = "", + verbose: bool = False, **kwargs): """Applies the DataNodeToTempTrans to the input arguments. :param node: The datanode to extract. :param storage_name: The base name of the temporary variable to store the result of the input node in. The default is tmp(_...) based on the rules defined in the SymbolTable class. + :param verbose: Whether to add comments to the input node if + the transformation fails. """ # Call validate to check inputs are valid. - self.validate(node, storage_name=storage_name, **kwargs) + self.validate(node, storage_name=storage_name, verbose=verbose, + **kwargs) # Find the datatype datatype = node.datatype @@ -359,12 +394,14 @@ def apply(self, node: DataNode, storage_name: str = "", **kwargs): # If we can hoist the allocate, find the highest level Loop # ancestor and set the schedule and position to place the # allocate before this loop. + # TODO #1445: Use HositTrans to do this if its extended to support + # more node types. if hoistable: loop_anc = schedule.ancestor(Loop) - finger = loop_anc - while finger: - loop_anc = finger - finger = finger.ancestor(Loop) + cursor = loop_anc + while cursor: + loop_anc = cursor + cursor = cursor.ancestor(Loop) if loop_anc: pos = loop_anc.position schedule = loop_anc.ancestor(Schedule) diff --git a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py index 688e27c563..f2837fed3a 100644 --- a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py +++ b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py @@ -70,10 +70,8 @@ _type_of_intrinsic_with_precision_of_named_arg, _findloc_return_type, _int_return_type, - _iparity_return_type, _get_bound_function_return_type, _matmul_return_type, - _maxval_return_type, ) from psyclone.psyir.symbols import ( ArrayType, @@ -1390,38 +1388,6 @@ def test_int_return_type(fortran_reader): assert rtype.precision.symbol.name == "wp" -def test_iparity_return_type(fortran_reader): - """Test the _iparity_return_type helper function.""" - code = """ - subroutine x - integer, dimension(100, 100) :: array - integer :: k - k = IPARITY(array) - end subroutine x - """ - psyir = fortran_reader.psyir_from_source(code) - # TODO #3268 Can't iparity directily with fortran reader, so need to - # create the Intrinsics manually using the psyir from the generated code. - intrinsic = psyir.walk(Call)[0] - intrinsic = IntrinsicCall.create( - IntrinsicCall.Intrinsic.IPARITY, - [x.copy() for x in intrinsic.arguments] - ) - - assert _iparity_return_type(intrinsic) == INTEGER_TYPE - - k_sym = psyir.children[0].symbol_table.lookup("k") - intrinsic = psyir.walk(Call)[0] - intrinsic = IntrinsicCall.create( - IntrinsicCall.Intrinsic.IPARITY, - [("array", intrinsic.arguments[0].copy()), ("dim", Reference(k_sym))], - ) - res = _iparity_return_type(intrinsic) - assert isinstance(res, ArrayType) - assert len(res.shape) == 1 - assert res.shape[0] == ArrayType.Extent.DEFERRED - - def test_get_bound_function_return_type(fortran_reader): """Test the _get_bound_function_return_type helper function.""" code = """subroutine x @@ -1556,45 +1522,6 @@ def test_matmul_return_type(fortran_reader): assert res.shape[1].upper.arguments[1].value == "2" -def test_maxval_return_type(fortran_reader): - '''Test for the _maxval_return_type function.''' - code = """subroutine test - integer, parameter :: wp = 8 - integer*8, dimension(100,100) :: x - integer, dimension(100) :: z - integer(kind=wp), dimension(100) :: m - integer :: y - y = MAXVAL(x) - z = MAXVAL(x, dim=2) - y = MAXVAL(m) - end subroutine test - """ - psyir = fortran_reader.psyir_from_source(code) - intrs = psyir.walk(IntrinsicCall) - - # Input is a int*8 so the return type should be an int*8 - res = _maxval_return_type(intrs[0]) - assert res.intrinsic == ScalarType.Intrinsic.INTEGER - assert res.precision == 8 - - # Input is a 2D array of int*8 with dim specified so the result - # is a 1D int*8 array. - res = _maxval_return_type(intrs[1]) - assert isinstance(res, ArrayType) - assert res.intrinsic == ScalarType.Intrinsic.INTEGER - assert res.precision == 8 - assert len(res.shape) == 1 - assert res.shape[0] == ArrayType.Extent.DEFERRED - - # Input is a 1D array of int(kind=wp) so the result is an - # int(kind=wp) - res = _maxval_return_type(intrs[2]) - assert isinstance(res, ScalarType) - assert res.intrinsic == ScalarType.Intrinsic.INTEGER - assert isinstance(res.precision, Reference) - assert res.precision.symbol.name == "wp" - - @pytest.mark.parametrize( "code, expected", [ diff --git a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py index 72c55e3416..f07b025010 100644 --- a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py @@ -51,7 +51,7 @@ from psyclone.tests.utilities import Compile -def test_datanodetotemptrans_validate(fortran_reader, tmp_path): +def test_datanodetotemptrans_validate(fortran_reader): """Tests the non-import related functionality of the validate function of the DataNodeToTempTrans.""" dtrans = DataNodeToTempTrans() @@ -62,10 +62,12 @@ def test_datanodetotemptrans_validate(fortran_reader, tmp_path): psyir = fortran_reader.psyir_from_source(code) assign = psyir.walk(Assignment)[0] with pytest.raises(TransformationError) as err: - dtrans.validate(assign.rhs) + dtrans.validate(assign.rhs, verbose=True) assert ("Input node's datatype is an array of unknown size, so the " "DataNodeToTempTrans cannot be applied. Input node was " "'b + a'" in str(err.value)) + assert ("PSyclone Warning: Input node's datatype is an array" in + assign.preceding_comment) code = """subroutine test use some_mod @@ -74,13 +76,17 @@ def test_datanodetotemptrans_validate(fortran_reader, tmp_path): psyir = fortran_reader.psyir_from_source(code) assign = psyir.walk(Assignment)[0] with pytest.raises(TransformationError) as err: - dtrans.validate(assign.rhs) + dtrans.validate(assign.rhs, verbose=True) assert ("The datatype of the supplied node cannot be computed, so " "the DataNodeToTempTrans cannot be applied. Input node " "was 'b + a'. The following symbols in the input " "node have not been resolved by PSyclone: '['a', 'b']'. " "Setting RESOLVE_IMPORTS in the transformation script " "may enable resolution of these symbols." in str(err.value)) + assert ("PSyclone Warning: The datatype of the supplied node " in + assign.preceding_comment) + assert ("Setting RESOLVE_IMPORTS in the transformation script" in + assign.preceding_comment) code = """subroutine test complex :: a, b @@ -90,10 +96,12 @@ def test_datanodetotemptrans_validate(fortran_reader, tmp_path): psyir = fortran_reader.psyir_from_source(code) assign = psyir.walk(Assignment)[0] with pytest.raises(TransformationError) as err: - dtrans.validate(assign.rhs) + dtrans.validate(assign.rhs, verbose=True) assert ("The datatype of the supplied node cannot be computed, " "so the DataNodeToTempTrans cannot be applied. Input node " "was 'a'" in str(err.value)) + assert ("PSyclone Warning: The datatype of the supplied node " in + assign.preceding_comment) with pytest.raises(TypeError) as err: dtrans.validate("abc") @@ -128,11 +136,13 @@ def test_datanodetotemptrans_validate(fortran_reader, tmp_path): psyir = fortran_reader.psyir_from_source(code) assign = psyir.walk(Assignment)[2] with pytest.raises(TransformationError) as err: - dtrans.validate(assign.rhs) + dtrans.validate(assign.rhs, verbose=True) assert ("Input node to DataNodeToTempTrans contains a call " "'some_func(a, b)' that is not " "guaranteed to be pure. Input node is 'a + some_func(a, b)'." in str(err.value)) + assert ("PSyclone Warning: Input node to DataNodeToTempTrans contains a " + "call" in assign.preceding_comment) def test_datanodetotemptrans_validate_imports( @@ -160,10 +170,12 @@ def test_datanodetotemptrans_validate_imports( psyir.children[0].symbol_table.resolve_imports() assign = psyir.walk(Assignment)[0] with pytest.raises(TransformationError) as err: - dtrans.validate(assign.rhs) + dtrans.validate(assign.rhs, verbose=True) assert ("The type of the node supplied to DataNodeToTempTrans depends " "upon an imported symbol 'i' which has a name clash with a " "symbol in the current scope." in str(err.value)) + assert ("PSyclone Warning: The type of the node supplied to " + in assign.preceding_comment) # This should work if the i in scope is imported from the # some_mod already. @@ -240,11 +252,13 @@ def test_datanodetotemptrans_validate_imports( psyir = FortranReader(resolve_modules=True).psyir_from_source(code) assign = psyir.walk(Assignment)[0] with pytest.raises(TransformationError) as err: - dtrans.validate(assign.rhs) + dtrans.validate(assign.rhs, verbose=True) assert ("Input node contains an imported symbol 'i' whose containing " "module collides with an existing symbol. Colliding name is " "'tmpmod'." in str(err.value)) + assert ("PSyclone Warning: Input node contains an imported symbol 'i' " + in assign.preceding_comment) filename = tmp_path / "some_other_mod.f90" with open(filename, "w", encoding='UTF-8') as module: @@ -264,14 +278,15 @@ def test_datanodetotemptrans_validate_imports( psyir = FortranReader(resolve_modules=True).psyir_from_source(code) assign = psyir.walk(Assignment)[0] with pytest.raises(TransformationError) as err: - dtrans.validate(assign.rhs) + dtrans.validate(assign.rhs, verbose=True) assert ("The datatype of the node suppled to DataNodeToTempTrans depends " "upon an imported symbol 'dim1' that is declared as private in " "its containing module, so cannot be imported." in str(err.value)) + assert ("PSyclone Warning: The datatype of the node suppled to" in + assign.preceding_comment) -def test_datanodetotemptrans_apply(fortran_reader, fortran_writer, tmp_path, - monkeypatch): +def test_datanodetotemptrans_apply(fortran_reader, fortran_writer, tmp_path): """Tests the apply function of the DataNodeToTempTrans without imported symbols.""" dtrans = DataNodeToTempTrans() From 78b4345e3bb05b620aab33344ce0f2023ef6ad36 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Fri, 24 Apr 2026 13:34:42 +0100 Subject: [PATCH 20/25] Revert changes due to issue with structure reference's datatype --- src/psyclone/psyir/nodes/intrinsic_call.py | 77 +++++++++++++++---- .../tests/psyir/nodes/intrinsic_call_test.py | 75 ++++++++++++++++++ 2 files changed, 138 insertions(+), 14 deletions(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index a098f6b76a..741e590998 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -259,7 +259,6 @@ def _type_of_named_arg_accounting_for_dim_arg( not isinstance(arg_dt.elemental_type, ScalarType) or not isinstance(arg_dt.elemental_type.intrinsic, ScalarType.Intrinsic) ): - print("?", arg_dt, type(arg_dt), arg_dt.elemental_type) return UnresolvedType() dtype = arg_dt.elemental_type # If dim is not present, return the same datatype @@ -428,6 +427,36 @@ def _int_return_type(node: IntrinsicCall) -> DataType: ) +def _iparity_return_type(node: IntrinsicCall) -> DataType: + """Helper function for the IPARITY case. + + The result is the same type as the "array" argument. If the + "dim" argument is not present, a scalar of that type is returned. + Otherwise an ArrayType of rank n-1 (where n is the rank of "array") of + that type is returned instead. + + :param node: The IntrinsicCall whose return type to compute. + + :returns: the computed datatype for the IntrinsicCall. + """ + # TODO #3415: Replace with _type_of_named_arg_accounting_for_dim_arg( + # node, "array"). + dtype = ScalarType( + node.argument_by_name("array").datatype.intrinsic, + node.argument_by_name("array").datatype.precision, + ) + # If dim is not present then we return a scalar. + if "dim" not in node.argument_names: + return dtype + # We have a dimension specified. We don't know the resultant shape + # in any detail as its dependent on the value of dim + return ArrayType( + dtype, + [ArrayType.Extent.DEFERRED] + * (len(node.argument_by_name("array").datatype.shape) - 1), + ) + + def _get_bound_function_return_type(node: IntrinsicCall) -> DataType: """Helper function for the return types of functions like LBOUND and LCOBOUND etc. @@ -525,6 +554,33 @@ def _matmul_return_type(node: IntrinsicCall) -> DataType: return ArrayType(stype, shape) +def _maxval_return_type(node: IntrinsicCall) -> DataType: + """ Helper function for the MAXVAL (and similar) intrinsic return + types. + + If the "dim" argument is absent, or the "array" argument has rank one + then the result is a ScalarType of the type of the "array" argument. + Otherwise the result is an ArrayType of rank n-1 (where n is the rank of + the "array" argument) with the same datatype of the "array" argument. + + :param node: The IntrinsicCall whose return type to compute. + + :returns: the computed datatype for the IntrinsicCall. + """ + # TODO #3415: Replace with _type_of_named_arg_accounting_for_dim_arg( + # node, "array"). + dtype = ScalarType( + node.argument_by_name("array").datatype.intrinsic, + node.argument_by_name("array").datatype.precision + ) + arg = node.argument_by_name("array") + if "dim" not in node.argument_names: + return dtype + # We have a dimension specified. We don't know the resultant shape + # in any detail as its dependent on the value of dim + return _type_of_arg_with_rank_minus_one(arg, dtype) + + def _dot_product_return_type(node: IntrinsicCall) -> DataType: """Helper value for DOT_PRODUCT intrinsic return type. @@ -2802,9 +2858,7 @@ class Intrinsic(IAttr, Enum): ) ), optional_args={"mask": DataNode}, - return_type=lambda node: ( - _type_of_named_arg_accounting_for_dim_arg(node, "array") - ), + return_type=_iparity_return_type, reference_accesses=lambda node: ( _compute_reference_accesses( node @@ -3169,7 +3223,8 @@ class Intrinsic(IAttr, Enum): return_type=lambda node: ( _type_of_scalar_with_optional_kind( node, - node.argument_by_name("l").datatype.intrinsic, + node.argument_by_name("l"). + datatype.intrinsic, "kind", ) if "kind" in node.argument_names else _type_of_named_argument(node, "l") @@ -3327,9 +3382,7 @@ class Intrinsic(IAttr, Enum): ) ), optional_args={"mask": DataNode}, - return_type=lambda node: ( - _type_of_named_arg_accounting_for_dim_arg(node, "array") - ), + return_type=_maxval_return_type, reference_accesses=lambda node: ( _compute_reference_accesses( node @@ -3458,9 +3511,7 @@ class Intrinsic(IAttr, Enum): ) ), optional_args={"mask": DataNode}, - return_type=lambda node: ( - _type_of_named_arg_accounting_for_dim_arg(node, "array") - ), + return_type=_maxval_return_type, reference_accesses=lambda node: ( _compute_reference_accesses( node, @@ -4015,9 +4066,7 @@ class Intrinsic(IAttr, Enum): optional_args={"mask": DataNode, "identity": DataNode, "ordered": DataNode}, - return_type=lambda node: ( - _type_of_named_arg_accounting_for_dim_arg(node, "array") - ), + return_type=_maxval_return_type, reference_accesses=lambda node: ( _compute_reference_accesses( node diff --git a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py index f2837fed3a..ae1a7053ce 100644 --- a/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py +++ b/src/psyclone/tests/psyir/nodes/intrinsic_call_test.py @@ -70,8 +70,10 @@ _type_of_intrinsic_with_precision_of_named_arg, _findloc_return_type, _int_return_type, + _iparity_return_type, _get_bound_function_return_type, _matmul_return_type, + _maxval_return_type, ) from psyclone.psyir.symbols import ( ArrayType, @@ -1388,6 +1390,39 @@ def test_int_return_type(fortran_reader): assert rtype.precision.symbol.name == "wp" +def test_iparity_return_type(fortran_reader): + """Test the _iparity_return_type helper function.""" + code = """ + subroutine x + integer, dimension(100, 100) :: array + integer :: k + k = IPARITY(array) + end subroutine x + """ + # TODO #3415: Test is superfluous with this issue fixed. + psyir = fortran_reader.psyir_from_source(code) + # TODO #3268 Can't iparity directily with fortran reader, so need to + # create the Intrinsics manually using the psyir from the generated code. + intrinsic = psyir.walk(Call)[0] + intrinsic = IntrinsicCall.create( + IntrinsicCall.Intrinsic.IPARITY, + [x.copy() for x in intrinsic.arguments] + ) + + assert _iparity_return_type(intrinsic) == INTEGER_TYPE + + k_sym = psyir.children[0].symbol_table.lookup("k") + intrinsic = psyir.walk(Call)[0] + intrinsic = IntrinsicCall.create( + IntrinsicCall.Intrinsic.IPARITY, + [("array", intrinsic.arguments[0].copy()), ("dim", Reference(k_sym))], + ) + res = _iparity_return_type(intrinsic) + assert isinstance(res, ArrayType) + assert len(res.shape) == 1 + assert res.shape[0] == ArrayType.Extent.DEFERRED + + def test_get_bound_function_return_type(fortran_reader): """Test the _get_bound_function_return_type helper function.""" code = """subroutine x @@ -1522,6 +1557,46 @@ def test_matmul_return_type(fortran_reader): assert res.shape[1].upper.arguments[1].value == "2" +def test_maxval_return_type(fortran_reader): + '''Test for the _maxval_return_type function.''' + # TODO #3415: Test is superfluous with this issue fixed. + code = """subroutine test + integer, parameter :: wp = 8 + integer*8, dimension(100,100) :: x + integer, dimension(100) :: z + integer(kind=wp), dimension(100) :: m + integer :: y + y = MAXVAL(x) + z = MAXVAL(x, dim=2) + y = MAXVAL(m) + end subroutine test + """ + psyir = fortran_reader.psyir_from_source(code) + intrs = psyir.walk(IntrinsicCall) + + # Input is a int*8 so the return type should be an int*8 + res = _maxval_return_type(intrs[0]) + assert res.intrinsic == ScalarType.Intrinsic.INTEGER + assert res.precision == 8 + + # Input is a 2D array of int*8 with dim specified so the result + # is a 1D int*8 array. + res = _maxval_return_type(intrs[1]) + assert isinstance(res, ArrayType) + assert res.intrinsic == ScalarType.Intrinsic.INTEGER + assert res.precision == 8 + assert len(res.shape) == 1 + assert res.shape[0] == ArrayType.Extent.DEFERRED + + # Input is a 1D array of int(kind=wp) so the result is an + # int(kind=wp) + res = _maxval_return_type(intrs[2]) + assert isinstance(res, ScalarType) + assert res.intrinsic == ScalarType.Intrinsic.INTEGER + assert isinstance(res.precision, Reference) + assert res.precision.symbol.name == "wp" + + @pytest.mark.parametrize( "code, expected", [ From 93c0b2021ac56a5bb7a971b90786d009b2a83185 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Fri, 24 Apr 2026 13:35:35 +0100 Subject: [PATCH 21/25] linting --- src/psyclone/psyir/transformations/datanode_to_temp_trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index 30d7b1df7d..889c05b74f 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -218,7 +218,7 @@ def validate(self, node: DataNode, **kwargs): ) if verbose: node.ancestor(Statement).\ - append_preceding_comment( + append_preceding_comment( f"PSyclone Warning: {message}" ) raise TransformationError(message) From 51c250afabaad061f50dc4f8c5ce3a27b4658ff6 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Fri, 24 Apr 2026 14:12:19 +0100 Subject: [PATCH 22/25] DataNodeToTempTrans will sometimes not make an allocatable if the compiler can do better --- .../transformations/datanode_to_temp_trans.py | 31 ++++++++++++++----- .../datanode_to_temp_trans_test.py | 17 +++++----- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index 889c05b74f..22402d165a 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -44,6 +44,7 @@ DataNode, IfBlock, IntrinsicCall, + Literal, Loop, Range, Reference, @@ -319,13 +320,27 @@ def apply(self, node: DataNode, storage_name: str = "", # the datatype to use the in-scope symbols datatype.replace_symbols_using(node.scope.symbol_table) - # We want to create an allocatable symbol for Array entities, so - # create a new datatype for the symbol and keep the - # datatype around for the ALLOCATE statement later. - allocatable_datatype = datatype - datatype = ArrayType(allocatable_datatype.elemental_type, - [ArrayType.Extent.DEFERRED for x in - allocatable_datatype.shape]) + # If any of the bound information aren't static then we need + # to create an allocatable array. + is_static = True + for element in datatype.shape: + if not isinstance(element.lower, Literal): + is_static = False + break + if not isinstance(element.upper, Literal): + is_static = False + break + if is_static: + datatype = ArrayType(datatype.elemental_type, + [x.copy() for x in datatype.shape]) + else: + # We want to create an allocatable symbol for Array entities, + # so create a new datatype for the symbol and keep the + # datatype around for the ALLOCATE statement later. + allocatable_datatype = datatype + datatype = ArrayType(allocatable_datatype.elemental_type, + [ArrayType.Extent.DEFERRED for x in + allocatable_datatype.shape]) # Create a symbol of the relevant type. if not storage_name: @@ -360,7 +375,7 @@ def apply(self, node: DataNode, storage_name: str = "", # If the datatype is an array, we need to allocate the array # before the statement too if its not already allocated. - if isinstance(datatype, ArrayType): + if isinstance(datatype, ArrayType) and not is_static: # Create an array reference to the symbol with the dimensions # returned by the datatype call earlier. ref = ArrayReference.create( diff --git a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py index f07b025010..647ecba4c3 100644 --- a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py @@ -343,8 +343,10 @@ def test_datanodetotemptrans_apply(fortran_reader, fortran_writer, tmp_path): enddo""" in out code = """subroutine test() + integer, parameter :: i = 1 + integer, parameter :: j = 3 integer, dimension(2:6) :: a - integer, dimension(1:3) :: b + integer, dimension(i:j) :: b a(2:4) = 3 * b @@ -356,7 +358,7 @@ def test_datanodetotemptrans_apply(fortran_reader, fortran_writer, tmp_path): assert """ integer, allocatable, dimension(:) :: tmp if (.NOT.ALLOCATED(tmp)) then - ALLOCATE(tmp(1:3)) + ALLOCATE(tmp(i:j)) end if tmp = 3 * b a(:4) = tmp""" in out @@ -414,11 +416,8 @@ def test_datanodetotemptrans_apply_imports( assign = psyir.walk(Assignment)[0] dtrans.apply(assign.rhs) out = fortran_writer(psyir) - assert """ integer, allocatable, dimension(:,:) :: tmp + assert """ integer, dimension(25,50) :: tmp - if (.NOT.ALLOCATED(tmp)) then - ALLOCATE(tmp(1:25,1:50)) - end if tmp = some_var b = tmp""" in out @@ -535,8 +534,8 @@ def test_datanodetotemptrans_hoistable_array(fortran_reader, code = """subroutine test use some_mod, only: some_func - integer :: i - real, dimension(100,100) :: arr1, arr2 + integer :: i, a, b + real, dimension(a,b) :: arr1, arr2 do i = 1, 100 call some_func(arr1*arr2) @@ -551,7 +550,7 @@ def test_datanodetotemptrans_hoistable_array(fortran_reader, out = fortran_writer(psyir) assert """ if (.NOT.ALLOCATED(tmp)) then - ALLOCATE(tmp(1:100,1:100)) + ALLOCATE(tmp(1:a,1:b)) end if do i = 1, 100, 1 tmp = arr1 * arr2 From 18dae4e31d8fac25167916bfb5f8903d6029060b Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Mon, 27 Apr 2026 14:31:29 +0100 Subject: [PATCH 23/25] Changes for review --- src/psyclone/psyir/nodes/intrinsic_call.py | 30 +++++++------------ .../transformations/datanode_to_temp_trans.py | 10 +++---- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index 741e590998..1a84fd5c37 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -3223,8 +3223,7 @@ class Intrinsic(IAttr, Enum): return_type=lambda node: ( _type_of_scalar_with_optional_kind( node, - node.argument_by_name("l"). - datatype.intrinsic, + node.argument_by_name("l").datatype.intrinsic, "kind", ) if "kind" in node.argument_names else _type_of_named_argument(node, "l") @@ -3772,10 +3771,8 @@ class Intrinsic(IAttr, Enum): optional_args={"vector": DataNode}, return_type=lambda node: ArrayType( ScalarType( - node.argument_by_name("array").datatype. - intrinsic, - node.argument_by_name("array").datatype. - precision), + node.argument_by_name("array").datatype.intrinsic, + node.argument_by_name("array").datatype.precision), [ArrayType.Extent.DEFERRED] ), reference_accesses=lambda node: ( @@ -4455,10 +4452,8 @@ class Intrinsic(IAttr, Enum): optional_args={}, return_type=lambda node: ArrayType( ScalarType( - node.argument_by_name("source").datatype. - intrinsic, - node.argument_by_name("source").datatype. - precision), + node.argument_by_name("source").datatype.intrinsic, + node.argument_by_name("source").datatype.precision), ([ArrayType.Extent.DEFERRED] * (len(node.argument_by_name("source").datatype.shape) + 1) if isinstance(node.argument_by_name("source").datatype, @@ -4727,10 +4722,8 @@ class Intrinsic(IAttr, Enum): ArrayType)) else ArrayType( ScalarType( - node.argument_by_name("mold").datatype. - intrinsic, - node.argument_by_name("mold").datatype. - precision + node.argument_by_name("mold").datatype.intrinsic, + node.argument_by_name("mold").datatype.precision ), [ArrayType.Extent.DEFERRED]) ), @@ -4752,10 +4745,8 @@ class Intrinsic(IAttr, Enum): arg_names=(("matrix",),)), optional_args={}, return_type=lambda node: ArrayType(ScalarType( - node.argument_by_name("matrix").datatype. - intrinsic, - node.argument_by_name("matrix").datatype. - precision), + node.argument_by_name("matrix").datatype.intrinsic, + node.argument_by_name("matrix").datatype.precision), [node.argument_by_name("matrix").datatype.shape[1], node.argument_by_name("matrix").datatype.shape[0]] ), @@ -4923,8 +4914,7 @@ def datatype(self) -> DataType: or "ScalarType expected 'precision' argument to be of " "type " in str(err)): return UnresolvedType() - # Is this reachable? Tested via monkeypatch as there may be - # some edge case I can't think of. + # This should never happen, propogate as an InternalError. outerr = err except AttributeError as err: # This is to handle when we call .intrinsic or diff --git a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py index 22402d165a..78505d31ef 100644 --- a/src/psyclone/psyir/transformations/datanode_to_temp_trans.py +++ b/src/psyclone/psyir/transformations/datanode_to_temp_trans.py @@ -322,15 +322,15 @@ def apply(self, node: DataNode, storage_name: str = "", # If any of the bound information aren't static then we need # to create an allocatable array. - is_static = True + has_static_bounds = True for element in datatype.shape: if not isinstance(element.lower, Literal): - is_static = False + has_static_bounds = False break if not isinstance(element.upper, Literal): - is_static = False + has_static_bounds = False break - if is_static: + if has_static_bounds: datatype = ArrayType(datatype.elemental_type, [x.copy() for x in datatype.shape]) else: @@ -375,7 +375,7 @@ def apply(self, node: DataNode, storage_name: str = "", # If the datatype is an array, we need to allocate the array # before the statement too if its not already allocated. - if isinstance(datatype, ArrayType) and not is_static: + if isinstance(datatype, ArrayType) and not has_static_bounds: # Create an array reference to the symbol with the dimensions # returned by the datatype call earlier. ref = ArrayReference.create( From b89289f61abc1fb40718dfed3a0a405116bf6af3 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Mon, 27 Apr 2026 14:32:03 +0100 Subject: [PATCH 24/25] changes for review --- examples/nemo/scripts/omp_gpu_trans.py | 2 -- examples/nemo/scripts/utils.py | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/nemo/scripts/omp_gpu_trans.py b/examples/nemo/scripts/omp_gpu_trans.py index 01a4fa2f0c..ba26aee5f9 100755 --- a/examples/nemo/scripts/omp_gpu_trans.py +++ b/examples/nemo/scripts/omp_gpu_trans.py @@ -190,8 +190,6 @@ def trans(psyir): if "pp_len" not in symtab: symtab.add(symtab.lookup("pp_len")) - # Extract any array operations from iom_put calls to temporary - # expressions that can be parallelised. normalise_loops( subroutine, hoist_local_arrays=False, diff --git a/examples/nemo/scripts/utils.py b/examples/nemo/scripts/utils.py index 72c608f707..40005514a5 100755 --- a/examples/nemo/scripts/utils.py +++ b/examples/nemo/scripts/utils.py @@ -542,19 +542,21 @@ def _satisfies_minimum_region_rules(self, region: list[Node]) -> bool: def iom_put_argument_to_temporary(calls: list[Call]): '''Extracts the second argument of all iom_put calls and puts them - in a temporary if they are an Operation with an array datatype.''' + in a temporary if they are an Operation with an array datatype. + + :param calls: The list of calls in a subroutine whose arguments + may be moved into temporary storage to allow additional potential + parallelisation. + + ''' for call in calls: if call.symbol.name == "iom_put": for arg in call.arguments: dtype = arg.datatype if (isinstance(dtype, ArrayType) and - isinstance(arg, Operation)): + (isinstance(arg, Operation) or + isinstance(arg, IntrinsicCall))): try: - DataNodeToTempTrans().apply(arg) - except TransformationError as err: - call.append_preceding_comment( - f"PSyclone Warning: Couldn't pull the argument " - f"{arg.debug_string().rstrip()} to a " - f"temporary due to the following error: " - f"{str(err.value)}" - ) + DataNodeToTempTrans().apply(arg, verbose=True) + except TransformationError: + pass From 20ba60a22ea7ddd43cc641a816b280fc31ded78f Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Tue, 28 Apr 2026 17:43:42 +0100 Subject: [PATCH 25/25] #3412 Update changelog --- changelog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog b/changelog index 56d32e873b..e3d24e129c 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,6 @@ + 13) PR #3373 towards #3412. Extract argument expressions from NEMO iom_put + calls in order to parallelise/offload their implicit loops. + 12) PR #3408 for #2812. Updates the Min/Max to code transformations so that they use the datatype of their arguments rather than assuming real. Also skips treesitter tests for Python < 3.10.