diff --git a/changelog b/changelog index 895ca4ebf6..335e026d04 100644 --- a/changelog +++ b/changelog @@ -256,6 +256,8 @@ 89) PR #2712 for #2711. Adds support for user-supplied Kernels that operate on dofs (in the LFRic API). + + 99) PR #2685 for #2027. Improves datatype and shape inference. release 2.5.0 14th of February 2024 diff --git a/examples/nemo/scripts/omp_cpu_trans.py b/examples/nemo/scripts/omp_cpu_trans.py index aa4258ba69..ceba2eadf6 100755 --- a/examples/nemo/scripts/omp_cpu_trans.py +++ b/examples/nemo/scripts/omp_cpu_trans.py @@ -47,7 +47,6 @@ # List of all files that psyclone will skip processing FILES_TO_SKIP = NOT_PERFORMANT + [ - "lbclnk.f90", # TODO #2685: effective shape bug "asminc.f90", "trosk.f90", "vremap.f90", diff --git a/examples/nemo/scripts/utils.py b/examples/nemo/scripts/utils.py index af1b5be162..39c98d9128 100755 --- a/examples/nemo/scripts/utils.py +++ b/examples/nemo/scripts/utils.py @@ -37,8 +37,8 @@ from psyclone.domain.common.transformations import KernelModuleInlineTrans from psyclone.psyir.nodes import ( - Loop, Assignment, Directive, Container, Reference, CodeBlock, Call, - Return, IfBlock, Routine, IntrinsicCall) + Assignment, Loop, Directive, Container, Reference, CodeBlock, + Call, Return, IfBlock, Routine, IntrinsicCall) from psyclone.psyir.symbols import ( DataSymbol, INTEGER_TYPE, REAL_TYPE, ArrayType, ScalarType, RoutineSymbol, ImportInterface) @@ -248,7 +248,7 @@ def normalise_loops( if convert_array_notation: # Make sure all array dimensions are explicit - for reference in schedule.walk(Reference, stop_type=Reference): + for reference in schedule.walk(Reference): part_of_the_call = reference.ancestor(Call) if part_of_the_call: if not part_of_the_call.is_elemental: diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index af421886d7..88fc6cc512 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1164,6 +1164,7 @@ def _parse_dimensions(self, dimensions, symbol_table): shape = [] # Traverse shape specs in Depth-first-search order for dim in walk(dimensions, (Fortran2003.Assumed_Shape_Spec, + Fortran2003.Deferred_Shape_Spec, Fortran2003.Explicit_Shape_Spec, Fortran2003.Assumed_Size_Spec)): @@ -1194,6 +1195,11 @@ def _parse_dimensions(self, dimensions, symbol_table): else: shape.append(None) + elif isinstance(dim, Fortran2003.Deferred_Shape_Spec): + # Deferred_Shape_Spec has no children (R520). For our purposes + # it is equivalent to Assumed_Shape_Spec(None, None). + shape.append(None) + elif isinstance(dim, Fortran2003.Explicit_Shape_Spec): upper = self._process_array_bound(dim.items[1], symbol_table) @@ -1680,7 +1686,8 @@ def _process_decln(self, scope, symbol_table, decl, visibility_map=None, has_save_attr = False if attr_specs: for attr in attr_specs.items: - if isinstance(attr, Fortran2003.Attr_Spec): + if isinstance(attr, (Fortran2003.Attr_Spec, + Fortran2003.Component_Attr_Spec)): normalized_string = str(attr).lower().replace(' ', '') if normalized_string == "save": if interface is not None: diff --git a/src/psyclone/psyir/nodes/array_mixin.py b/src/psyclone/psyir/nodes/array_mixin.py index 5c992f0638..7acb40380f 100644 --- a/src/psyclone/psyir/nodes/array_mixin.py +++ b/src/psyclone/psyir/nodes/array_mixin.py @@ -610,7 +610,8 @@ def _get_effective_shape(self): :rtype: list[:py:class:`psyclone.psyir.nodes.DataNode`] :raises NotImplementedError: if any of the array-indices involve a - function call or an expression. + function call or an expression or are + of unknown type. ''' shape = [] for idx, idx_expr in enumerate(self.indices): @@ -621,7 +622,7 @@ def _get_effective_shape(self): dtype = idx_expr.datatype if isinstance(dtype, ArrayType): # An array slice can be defined by a 1D slice of another - # array, e.g. `a(b(1:4))`. + # array, e.g. `a(b(1:4))` or `a(b)`. indirect_array_shape = dtype.shape if len(indirect_array_shape) > 1: raise NotImplementedError( @@ -630,8 +631,22 @@ def _get_effective_shape(self): f"used to index into '{self.name}' has " f"{len(indirect_array_shape)} dimensions.") # pylint: disable=protected-access - shape.append(idx_expr._extent(idx)) - + if isinstance(idx_expr, ArrayMixin): + shape.append(idx_expr._extent(idx)) + else: + # We have a Reference (to an array) with no explicit + # indexing. The extent of this is then the SIZE of + # that array. + sizeop = IntrinsicCall.create( + IntrinsicCall.Intrinsic.SIZE, [idx_expr.copy()]) + shape.append(sizeop) + elif isinstance(dtype, (UnsupportedType, UnresolvedType)): + raise NotImplementedError( + f"The array index expression " + f"'{idx_expr.debug_string()}' in access " + f"'{self.debug_string()}' is of '{dtype}' type and " + f"therefore whether it is an array slice (i.e. an " + f"indirect access) cannot be determined.") elif isinstance(idx_expr, (Call, Operation, CodeBlock)): # We can't yet straightforwardly query the type of a function # call or Operation - TODO #1799. diff --git a/src/psyclone/psyir/nodes/structure_reference.py b/src/psyclone/psyir/nodes/structure_reference.py index fd7c4e8842..f4be0a6f65 100644 --- a/src/psyclone/psyir/nodes/structure_reference.py +++ b/src/psyclone/psyir/nodes/structure_reference.py @@ -269,10 +269,34 @@ def datatype(self): :returns: the datatype of this reference. :rtype: :py:class:`psyclone.psyir.symbols.DataType` - :raises NotImplementedError: if the structure reference represents \ + :raises NotImplementedError: if the structure reference represents an array of arrays. ''' + def _get_cursor_shape(cursor, cursor_type): + ''' + Utility that returns the shape of the supplied node. + + :param cursor: the node to get the shape of. + :type cursor: :py:class:`psyclone.psyir.nodes.Node` + :param cursor_type: the type of the node. + :type cursor_type: :py:class:`psyclone.psyir.symbols.DataType` + + :returns: the shape of the node or None if it is not an array. + :rtype: Optional[list[:py:class:`psyclone.psyir.nodes.Node`]] + + ''' + if not (isinstance(cursor, ArrayMixin) or + isinstance(cursor_type, ArrayType)): + return None + if isinstance(cursor, ArrayMixin): + # It is an ArrayMixin and has indices so could be a single + # element or a slice. + # pylint: disable=protected-access + return cursor._get_effective_shape() + # No indices so it is an access to a whole array. + return cursor_type.shape + # pylint: disable=too-many-return-statements, too-many-branches if self._overwrite_datatype: return self._overwrite_datatype @@ -296,14 +320,10 @@ def datatype(self): # If the reference has explicit array syntax, we need to consider it # to calculate the resulting shape. - if isinstance(cursor, ArrayMixin): - try: - # pylint: disable=protected-access - shape = cursor._get_effective_shape() - except NotImplementedError: - return UnresolvedType() - else: - shape = [] + try: + shape = _get_cursor_shape(cursor, cursor_type) + except NotImplementedError: + return UnresolvedType() # Walk down the structure, collecting information on any array slices # as we go. @@ -317,46 +337,24 @@ def datatype(self): # Once we've hit an Unresolved/UnsupportedType the cursor_type # will remain set to that as we can't do any better. cursor_type = cursor_type.components[cursor.name].datatype - if isinstance(cursor, ArrayMixin): - # pylint: disable=protected-access - try: - shape.extend(cursor._get_effective_shape()) - except NotImplementedError: - return UnresolvedType() - - # We've reached the ultimate member of the structure access. - if shape: - if isinstance(cursor_type, ArrayType): - # It's of array type but does it represent a single element, - # a slice or a whole array? (We use `children` rather than - # `indices` so as to avoid having to check that `cursor` is - # an `ArrayMember`.) - if cursor.children: - # It has indices so could be a single element or a slice. - # pylint: disable=protected-access - cursor_shape = cursor._get_effective_shape() - else: - # No indices so it is an access to a whole array. - cursor_shape = cursor_type.shape - if cursor_shape and len(shape) != len(cursor_shape): - # This ultimate access is an array but we've already - # encountered one or more slices earlier in the access - # expression. + try: + cursor_shape = _get_cursor_shape(cursor, cursor_type) + except NotImplementedError: + return UnresolvedType() + if cursor_shape: + if shape: raise NotImplementedError( f"Array of arrays not supported: the ultimate member " f"'{cursor.name}' of the StructureAccess represents " f"an array but other array notation is present in the " f"full access expression: '{self.debug_string()}'") + shape = cursor_shape + if shape: return ArrayType(cursor_type, shape) - # We don't have an explicit array access (because `shape` is Falsey) - # but is the ultimate member itself an array? + # We must have a scalar. if isinstance(cursor_type, ArrayType): - if not cursor.children: - # It is and there are no index expressions so we return the - # ArrayType. - return cursor_type # We have an access to a single element of the array. # Currently arrays of scalars are handled in a # different way to all other types of array. Issue #1857 will diff --git a/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py b/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py index 0fc0238a5d..59a65b99ff 100644 --- a/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py +++ b/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py @@ -49,10 +49,10 @@ CodeBlock, Routine, BinaryOperation) from psyclone.psyir.nodes.array_mixin import ArrayMixin from psyclone.psyir.symbols import ( - DataSymbol, INTEGER_TYPE, ScalarType, UnresolvedType, SymbolError, - ArrayType) -from psyclone.psyir.transformations.transformation_error \ - import TransformationError + ArrayType, DataSymbol, INTEGER_TYPE, ScalarType, SymbolError, + UnresolvedType) +from psyclone.psyir.transformations.transformation_error import ( + TransformationError) from psyclone.psyir.transformations.reference2arrayrange_trans import ( Reference2ArrayRangeTrans) @@ -241,17 +241,16 @@ def validate(self, node, options=None): f"Range, but none were found in '{node.debug_string()}'.")) # All the ArrayMixins must have the same number of Ranges to expand - found = None + num_of_ranges = None for accessor in node.walk(ArrayMixin): - num_of_ranges = len([x for x in accessor.indices - if isinstance(x, Range)]) - if num_of_ranges > 0: - if not found: + count = len([x for x in accessor.indices if isinstance(x, Range)]) + if count > 0: + if not num_of_ranges: # If it's the first value we find, we store it - found = num_of_ranges + num_of_ranges = count else: # Otherwise we compare it against the previous found value - if found != num_of_ranges: + if count != num_of_ranges: raise TransformationError(LazyString( lambda: f"{self.name} does not support statements" f" containing array accesses that have varying " @@ -322,8 +321,8 @@ def validate(self, node, options=None): lambda: f"{message} in:\n{node.debug_string()}.")) # For each top-level reference (because we don't support nesting), the - # apply will have to be able to decide if its an Array (and access it - # with the index) or an Scalar (and leave it as it is). We can not + # apply() will have to determine whether it's an Array (and access it + # with the index) or a Scalar (and leave it as it is). We cannot # transform references where this is unclear. for reference in node.walk(Reference, stop_type=Reference): if isinstance(reference.parent, Call): @@ -351,12 +350,12 @@ def validate(self, node, options=None): # scalar. if not isinstance(reference.datatype, (ScalarType, ArrayType)): if isinstance(reference.symbol, DataSymbol): - typestr = f"an {reference.symbol.datatype}" + typestr = f"an {reference.datatype}" else: typestr = "not a DataSymbol" message = ( f"{self.name} cannot expand expression because it " - f"contains the variable '{reference.symbol.name}' " + f"contains the access '{reference.debug_string()}' " f"which is {typestr} and therefore cannot be guaranteed" f" to be ScalarType.") if not isinstance(reference.symbol, DataSymbol) or \ diff --git a/src/psyclone/psyir/transformations/inline_trans.py b/src/psyclone/psyir/transformations/inline_trans.py index 47da70ff55..d44df08d74 100644 --- a/src/psyclone/psyir/transformations/inline_trans.py +++ b/src/psyclone/psyir/transformations/inline_trans.py @@ -530,7 +530,10 @@ def _replace_formal_struc_arg(self, actual_arg, ref, call_node, break cursor = cursor.member - if not actual_arg.walk(Range) and local_indices: + # TODO #1858 - once we support converting structure accesses into + # explicit array accesses, we can put back the testing in + # inline_trans_test.py that covers this code and remove the pragma: + if not actual_arg.walk(Range) and local_indices: # pragma: no cover # There are no Ranges in the actual argument but the local # reference is an array access. # Create updated index expressions for that access. diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py index 3135b37a58..1e6a8c8553 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py @@ -850,6 +850,18 @@ def test_unsupported_decln(fortran_reader): assert ("Invalid variable declaration found in _process_decln for " "'problem'" in str(err.value)) + reader = FortranStringReader("integer, intent(in) :: l1") + fparser2spec = Specification_Part(reader).content[0] + # Break the attribute-spec list for this declaration. + attr_specs = fparser2spec.items[1] + attr_specs.items = (attr_specs.items[0], "rubbish") + fake_parent = KernelSchedule.create("dummy_schedule") + symtab = fake_parent.symbol_table + processor = Fparser2Reader() + with pytest.raises(NotImplementedError) as error: + processor._process_decln(fake_parent, symtab, fparser2spec) + assert "Unrecognised attribute type 'str'" in str(error.value) + def test_unsupported_decln_structure_type(fortran_reader): ''' diff --git a/src/psyclone/tests/psyir/frontend/fparser2_where_handler_test.py b/src/psyclone/tests/psyir/frontend/fparser2_where_handler_test.py index 3b51510dcb..b87f0666a2 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_where_handler_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_where_handler_test.py @@ -692,16 +692,24 @@ def test_where_stmt_validity(): @pytest.mark.usefixtures("parser") -def test_where_stmt(): +def test_where_stmt(fortran_reader): ''' Basic check that we handle a WHERE statement correctly. ''' - fake_parent, _ = process_where( - "WHERE( at_i(:,:) > rn_amax_2d(:,:) ) " - "a_i(:,:,jl) = a_i(:,:,jl) * rn_amax_2d(:,:) / at_i(:,:)", - Fortran2003.Where_Stmt, ["at_i", "rn_amax_2d", "jl", "a_i"]) - assert len(fake_parent.children) == 1 - assert isinstance(fake_parent[0], Loop) + code = '''\ + program where_test + implicit none + integer :: jl + real, dimension(:,:), allocatable :: at_i, rn_amax_2d + real, dimension(:,:,:), allocatable :: a_i + WHERE( at_i(:,:) > rn_amax_2d(:,:) ) & + a_i(:,:,jl) = a_i(:,:,jl) * rn_amax_2d(:,:) / at_i(:,:) + end program where_test + ''' + psyir = fortran_reader.psyir_from_source(code) + routine = psyir.walk(Routine)[0] + assert len(routine.children) == 1 + assert isinstance(routine[0], Loop) def test_where_stmt_no_reduction(fortran_reader, fortran_writer): @@ -765,11 +773,11 @@ def test_where_ordering(parser): ("where (my_type%var(:) > epsi20)\n" "my_type2(jl)%array2(:,jl) = 3.0\n", "my_type%var"), ("where (my_type%block(jl)%var(:) > epsi20)\n" - "my_type%block%array(:,jl) = 3.0\n", "my_type%block(jl)%var"), + "my_type%block(jl)%array(:,jl) = 3.0\n", "my_type%block(jl)%var"), ("where (my_type%block(jl)%var(:) > epsi20)\n" "my_type%block(jl)%array(:,jl) = 3.0\n", "my_type%block(jl)%var"), - ("where (my_type2(:)%var > epsi20)\n" - "my_type2(:)%var = 3.0\n", "my_type2")]) + ("where (my_type2(:)%var(jl) > epsi20)\n" + "my_type2(:)%var(jl) = 3.0\n", "my_type2")]) def test_where_derived_type(fortran_reader, code, size_arg): ''' Test that we handle the case where array members of a derived type are accessed within a WHERE. ''' @@ -805,10 +813,12 @@ def test_where_derived_type(fortran_reader, code, size_arg): assert isinstance(loops[1].loop_body[0], IfBlock) # All Range nodes should have been replaced assert not loops[0].walk(Range) - # All ArrayMember accesses should now use the `widx1` loop variable + # All ArrayMember accesses other than 'var' should now use the `widx1` + # loop variable array_members = loops[0].walk(ArrayMember) for member in array_members: - assert "+ widx1 - 1" in member.indices[0].debug_string() + if member.name != "var": + assert "+ widx1 - 1" in member.indices[0].debug_string() @pytest.mark.parametrize( diff --git a/src/psyclone/tests/psyir/nodes/array_mixin_test.py b/src/psyclone/tests/psyir/nodes/array_mixin_test.py index f9290e82b3..7c4630fed1 100644 --- a/src/psyclone/tests/psyir/nodes/array_mixin_test.py +++ b/src/psyclone/tests/psyir/nodes/array_mixin_test.py @@ -608,6 +608,7 @@ def test_get_effective_shape(fortran_reader): code = ( "subroutine test()\n" " use some_mod\n" + " integer :: idx = 2\n" " integer :: indices(8,3)\n" " real a(10), b(10,10)\n" " a(1:10) = 0.0\n" @@ -621,6 +622,8 @@ def test_get_effective_shape(fortran_reader): " b(indices(2:3,1:2), 2:5) = 2.0\n" " a(f()) = 2.0\n" " a(2+3) = 1.0\n" + " b(idx, a) = -1.0\n" + " b(scalarval, arrayval) = 1\n" "end subroutine\n") psyir = fortran_reader.psyir_from_source(code) routine = psyir.walk(Routine)[0] @@ -691,6 +694,49 @@ def test_get_effective_shape(fortran_reader): with pytest.raises(NotImplementedError) as err: _ = routine.children[child_idx].lhs._get_effective_shape() assert "include a function call or expression" in str(err.value) + # Array access with indices given by another array that is not explicitly + # indexed. + child_idx += 1 + shape = routine.children[child_idx].lhs._get_effective_shape() + assert len(shape) == 1 + assert "SIZE(a)" in shape[0].debug_string() + # Array-index expressions are symbols of unknown type so we don't know + # whether we have an array slice or just a scalar. + child_idx += 1 + with pytest.raises(NotImplementedError) as err: + _ = routine.children[child_idx].lhs._get_effective_shape() + assert ("index expression 'scalarval' in access 'b(scalarval,arrayval)' is" + " of 'UnresolvedType' type and therefore whether it is an array " + "slice (i.e. an indirect access) cannot be determined." + in str(err.value)) + + +def test_struct_get_effective_shape(fortran_reader): + '''Tests for the _get_effective_shape() method for ArrayMember and + ArrayOfStructuresMixin (since they inherit it from ArrayMixin).''' + code = ( + "subroutine test()\n" + " type :: my_type\n" + " real, dimension(21,12) :: data\n" + " end type my_type\n" + " type(my_type) :: grid\n" + " type(my_type), dimension(5) :: grid_list\n" + " grid%data(:,:) = 0.0\n" + " grid_list(:)%data(1) = 0.0\n" + "end subroutine\n") + psyir = fortran_reader.psyir_from_source(code) + routine = psyir.walk(Routine)[0] + # Slice of ArrayMember + child_idx = 0 + shape = routine.children[child_idx].lhs.member._get_effective_shape() + assert len(shape) == 2 + assert "SIZE(grid%data, dim=1)" in shape[0].debug_string() + assert "SIZE(grid%data, dim=2)" in shape[1].debug_string() + # Slice of ArrayOfStructuresMixin + child_idx += 1 + shape = routine.children[child_idx].lhs._get_effective_shape() + assert len(shape) == 1 + assert isinstance(shape[0], IntrinsicCall) # get_outer_range_index diff --git a/src/psyclone/tests/psyir/nodes/array_reference_test.py b/src/psyclone/tests/psyir/nodes/array_reference_test.py index 8073840657..027e812ac8 100644 --- a/src/psyclone/tests/psyir/nodes/array_reference_test.py +++ b/src/psyclone/tests/psyir/nodes/array_reference_test.py @@ -580,6 +580,13 @@ def test_array_datatype(): aref6._symbol = test_struc_sym assert isinstance(aref6.datatype, UnresolvedType) + # TODO #2448 - we don't handle an array access to something that we + # don't know is an array. + aref7 = ArrayReference(generic_sym) + aref7.addchild(one.copy()) + aref7._symbol = DataSymbol("int_test", INTEGER_TYPE) + assert isinstance(aref7.datatype, UnresolvedType) + def test_array_create_colon(fortran_writer): '''Test that the create method accepts ":" as shortcut to automatically diff --git a/src/psyclone/tests/psyir/nodes/structure_reference_test.py b/src/psyclone/tests/psyir/nodes/structure_reference_test.py index a8cf02a4ab..55881ccdd7 100644 --- a/src/psyclone/tests/psyir/nodes/structure_reference_test.py +++ b/src/psyclone/tests/psyir/nodes/structure_reference_test.py @@ -241,7 +241,9 @@ def test_struc_ref_datatype(): grid_type = symbols.StructureType.create([ ("nx", symbols.INTEGER_TYPE, symbols.Symbol.Visibility.PUBLIC, None), ("data", atype, symbols.Symbol.Visibility.PRIVATE, None), + # A single member of structure type. ("roger", rtype, symbols.Symbol.Visibility.PUBLIC, None), + # An array of structure type. ("titty", artype, symbols.Symbol.Visibility.PUBLIC, None)]) # Symbol with type defined by StructureType ssym0 = symbols.DataSymbol("grid", grid_type) @@ -273,6 +275,20 @@ def test_struc_ref_datatype(): ssym, [("titty", [one.copy(), two.copy()])]) assert singleref.datatype == rtype_sym + # Reference to grid%titty(1,2)%gibber + sref = nodes.StructureReference.create( + ssym, [("titty", [one.copy(), two.copy()]), "gibber"]) + assert sref.datatype == symbols.BOOLEAN_TYPE + + # Reference to grid%titty(my_func(1), 2) where my_func is unresolved. + func_sym = symbols.DataSymbol("my_func", + datatype=symbols.UnresolvedType(), + interface=symbols.UnresolvedInterface()) + fref = nodes.ArrayReference.create(func_sym, [one.copy()]) + sref2 = nodes.StructureReference.create( + ssym, [("titty", [fref, two.copy()])]) + assert isinstance(sref2.datatype, symbols.UnresolvedType) + # Reference to sub-array of structure members of structure myrange = nodes.Range.create(two.copy(), nodes.Literal("4", symbols.INTEGER_TYPE)) diff --git a/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py b/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py index e9ffea258f..95cb0be342 100644 --- a/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py @@ -303,6 +303,40 @@ def test_apply_to_arrays_with_different_bounds(fortran_reader, fortran_writer): in output_test4) +def test_apply_indirect_indexing(fortran_reader, fortran_writer): + ''' + Check the application of the transformation when the array is indexed + indirectly. + + ''' + psyir = fortran_reader.psyir_from_source(''' + program test + use dom_oce + INTEGER, DIMENSION(4) :: iwewe + INTEGER, DIMENSION(8,kfld) :: ishtSi + integer :: jf + do jf = 1, kfld + ishtSi(5:8,jf) = ishtSi(iwewe, jf) + ishtSi(1:4,jf) = grid(3)%var(iwewe, jf) + end do + end program test + ''') + assignments = psyir.walk(Assignment) + trans = ArrayAssignment2LoopsTrans() + trans.apply(assignments[0]) + trans.apply(assignments[1]) + result = fortran_writer(psyir) + assert (''' + do idx = 5, 8, 1 + ishtsi(idx,jf) = ishtsi(iwewe(idx + (1 - 5)),jf) + enddo''' in result) + assert (''' + do idx_1 = 1, 4, 1 + ishtsi(idx_1,jf) = grid(3)%var(iwewe(idx_1),jf) + enddo + enddo''' in result) + + def test_apply_outside_routine(fortran_reader, fortran_writer): ''' Check that the transformation still succeeds and generates valid code when found in a scope detached from a Routine. ''' @@ -568,15 +602,18 @@ def test_validate_rhs_plain_references(fortran_reader, fortran_writer): ''' psyir = fortran_reader.psyir_from_source(''' subroutine test(unsupported) - use my_variables, only: unresolved + use my_variables, only: unresolved, map integer :: scalar = 1 integer, dimension(:) :: array = 1, x integer, dimension(:), optional :: unsupported - + integer, dimension(4) :: ishtsi x(:) = scalar x(:) = array x(:) = unresolved x(:) = unsupported + ! An indirectly-addressed RHS but we can't tell whether or not map is + ! an array reference + x(:) = ishtsi(map,scalar) end subroutine test ''') @@ -594,7 +631,7 @@ def test_validate_rhs_plain_references(fortran_reader, fortran_writer): with pytest.raises(TransformationError) as info: trans.apply(assign_with_unresolved, opts) assert ("ArrayAssignment2LoopsTrans cannot expand expression because it " - "contains the variable 'unresolved' which is not a DataSymbol " + "contains the access 'unresolved' which is not a DataSymbol " "and therefore cannot be guaranteed to be ScalarType. Resolving " "the import that brings this variable into scope may help." in str(info.value)) @@ -604,18 +641,25 @@ def test_validate_rhs_plain_references(fortran_reader, fortran_writer): with pytest.raises(TransformationError) as info: trans.apply(assign_with_unresolved) assert ("ArrayAssignment2LoopsTrans cannot expand expression because it " - "contains the variable 'unresolved' which is an UnresolvedType " + "contains the access 'unresolved' which is an UnresolvedType " "and therefore cannot be guaranteed to be ScalarType. Resolving " "the import that brings this variable into scope may help." in str(info.value)) with pytest.raises(TransformationError) as info: trans.apply(psyir.walk(Assignment)[3], opts) assert ("ArrayAssignment2LoopsTrans cannot expand expression because it " - "contains the variable 'unsupported' which is an Unsupported" + "contains the access 'unsupported' which is an Unsupported" "FortranType('INTEGER, DIMENSION(:), OPTIONAL :: unsupported') " "and therefore cannot be guaranteed to be ScalarType." in str(info.value)) + with pytest.raises(TransformationError) as info: + trans.apply(psyir.walk(Assignment)[4], opts) + assert ("ArrayAssignment2LoopsTrans cannot expand expression because it " + "contains the access 'ishtsi(map,scalar)' which is an " + "UnresolvedType and therefore cannot be guaranteed to be " + "ScalarType" in str(info.value)) + # The end result should look like: assert ( " do idx = LBOUND(x, dim=1), UBOUND(x, dim=1), 1\n" @@ -625,15 +669,19 @@ def test_validate_rhs_plain_references(fortran_reader, fortran_writer): " x(idx_1) = array(idx_1)\n" " enddo\n" " ! ArrayAssignment2LoopsTrans cannot expand expression because it " - "contains the variable 'unresolved' which is not a DataSymbol and " + "contains the access 'unresolved' which is not a DataSymbol and " "therefore cannot be guaranteed to be ScalarType. Resolving the import" " that brings this variable into scope may help.\n" " x(:) = unresolved\n" " ! ArrayAssignment2LoopsTrans cannot expand expression because it " - "contains the variable 'unsupported' which is an UnsupportedFortran" + "contains the access 'unsupported' which is an UnsupportedFortran" "Type('INTEGER, DIMENSION(:), OPTIONAL :: unsupported') and therefore " "cannot be guaranteed to be ScalarType.\n" " x(:) = unsupported\n" + " ! ArrayAssignment2LoopsTrans cannot expand expression because it " + "contains the access 'ishtsi(map,scalar)' which is an UnresolvedType " + "and therefore cannot be guaranteed to be ScalarType.\n" + " x(:) = ishtsi(map,scalar)\n" ) in fortran_writer(psyir) @@ -685,7 +733,7 @@ def test_validate_non_elemental_functions(fortran_reader): with pytest.raises(TransformationError) as err: trans.validate(assignment3, options={"verbose": True}) errormsg = ("ArrayAssignment2LoopsTrans cannot expand expression because " - "it contains the variable 'myfunc' which is not a DataSymbol " + "it contains the access 'myfunc(y)' which is not a DataSymbol " "and therefore cannot be guaranteed to be ScalarType. " "Resolving the import that brings this variable into scope " "may help.") @@ -695,9 +743,95 @@ def test_validate_non_elemental_functions(fortran_reader): with pytest.raises(TransformationError) as err: trans.validate(assignment4, options={"verbose": True}) errormsg = ("ArrayAssignment2LoopsTrans cannot expand expression because " - "it contains the variable 'var' which is an UnresolvedType " - "and therefore cannot be guaranteed to be ScalarType. " - "Resolving the import that brings this variable into scope " - "may help.") + "it contains the access 'var%myfunc(y)' which is an " + "UnresolvedType and therefore cannot be guaranteed to be " + "ScalarType. Resolving the import that brings this variable " + "into scope may help.") assert errormsg in assignment4.preceding_comment assert errormsg in str(err.value) + + +def test_validate_indirect_indexing(fortran_reader): + ''' + Check the validation of the transformation when the array is indexed + indirectly in unsupported ways. + + ''' + psyir = fortran_reader.psyir_from_source(''' + program test + use dom_oce + INTEGER, DIMENSION(4) :: iwewe + INTEGER, DIMENSION(8,kfld) :: ishtSi + ! Assignment with CodeBlock on RHS. + iwewe(:) = (/ jpwe,jpea,jpwe,jpea /) + ! Assignment with CodeBlock in array-index expression. + iwewe(:) = ishtSi((/ jpwe,jpea,jpwe,jpea /), 1) + ! Index expression does evaluate to an array but we don't currently + ! handle getting a type in this case (TODO #1799). + ishtSi(5:8,jf) = ishtSi(iwewe+1, jf) + ! Index expression contains a call to an unknown function. + ishtSi(5:8,jf) = ishtSi(my_func(1), jf) + end program test + ''') + assignments = psyir.walk(Assignment) + assert isinstance(assignments[0].rhs, CodeBlock) + trans = ArrayAssignment2LoopsTrans() + with pytest.raises(TransformationError) as err: + trans.validate(assignments[0]) + assert ("ArrayAssignment2LoopsTrans does not support array assignments " + "that contain a CodeBlock anywhere in the expression" + in str(err.value)) + with pytest.raises(TransformationError) as err: + trans.validate(assignments[1]) + assert ("ArrayAssignment2LoopsTrans does not support array assignments " + "that contain a CodeBlock anywhere in the expression" + in str(err.value)) + # TODO #1799 - this requires that we extended ArrayMixin.datatype to + # support array accesses with index expressions that are Operations. + with pytest.raises(TransformationError) as err: + trans.validate(assignments[2]) + assert ("cannot expand expression because it contains the access " + "'ishtsi(iwewe + 1,jf)' which is an UnresolvedType and therefore " + "cannot be guaranteed" in str(err.value)) + with pytest.raises(TransformationError) as err: + trans.validate(assignments[3]) + assert ("cannot expand expression because it contains the access " + "'ishtsi(my_func(1),jf)' which is an UnresolvedType and therefore " + "cannot be guaranteed to be ScalarType." in str(err.value)) + + +def test_validate_structure(fortran_reader): + ''' + Check the validation of the transformation when it is a structure access + on the RHS. + + ''' + psyir = fortran_reader.psyir_from_source(''' + program test + integer, parameter :: ngrids = 4, kfld=5 + integer, dimension(8,kfld) :: ishtSi + type :: sub_grid_type + integer, dimension(kfld,kfld) :: map + end type sub_grid_type + type :: grid_type + real, dimension(:,:), allocatable :: data + type(sub_grid_type), dimension(ngrids) :: subgrid + end type grid_type + type(grid_type) :: grid + integer :: jf + ! Cannot tell whether or not the access on the RHS is an array. + ishtSi(5:8,jf) = grid%data(my_func(1), jf) + ! The array access to subgrid is not yet supported. + ishtSi(5:8,jf) = grid%subgrid%map(1,1) + end program test + ''') + assignments = psyir.walk(Assignment) + trans = ArrayAssignment2LoopsTrans() + with pytest.raises(TransformationError) as err: + trans.validate(assignments[0]) + assert ("contains the access 'grid%data(my_func(1),jf)' which is an " + "UnresolvedType" in str(err.value)) + # TODO #1858 - once we've extended Reference2ArrayRangeTrans to support + # StructureMembers we can use it as part of this transformation and this + # example will be supported. + trans.validate(assignments[1]) diff --git a/src/psyclone/tests/psyir/transformations/inline_trans_test.py b/src/psyclone/tests/psyir/transformations/inline_trans_test.py index 35748414aa..e7efd1c172 100644 --- a/src/psyclone/tests/psyir/transformations/inline_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/inline_trans_test.py @@ -833,10 +833,10 @@ def test_apply_struct_array_slice_arg(fortran_reader, fortran_writer, tmpdir): f" grid%data2d(:,:) = 1.0\n" f" do i=1,10\n" f" a(i) = 1.0\n" - f" call sub(grid%local%data)\n" f" call sub(micah%grids(3)%region%data(:))\n" f" call sub(grid%data2d(:,i))\n" f" call sub(grid%data2d(1:5,i))\n" + f" call sub(grid%local%data)\n" f" end do\n" f" end subroutine run_it\n" f" subroutine sub(x)\n" @@ -854,35 +854,42 @@ def test_apply_struct_array_slice_arg(fortran_reader, fortran_writer, tmpdir): inline_trans = InlineTrans() for call in psyir.walk(Call): if not isinstance(call, IntrinsicCall): + if call.arguments[0].debug_string() == "grid%local%data": + # TODO #1858: this if construct can be removed once we + # support getting the type of `grid%local%data`. + continue inline_trans.apply(call) output = fortran_writer(psyir) assert (" do i = 1, 10, 1\n" " a(i) = 1.0\n" " do ji = 1, 5, 1\n" - " grid%local%data(ji) = 2.0 * grid%local%data(ji)\n" - " enddo\n" - " grid%local%data(1:2) = 0.0\n" - " grid%local%data(:) = 3.0\n" - " grid%local%data = 5.0\n" - " do ji_1 = 1, 5, 1\n" - " micah%grids(3)%region%data(ji_1) = 2.0 * " - "micah%grids(3)%region%data(ji_1)\n" + " micah%grids(3)%region%data(ji) = 2.0 * " + "micah%grids(3)%region%data(ji)\n" " enddo\n" " micah%grids(3)%region%data(1:2) = 0.0\n" " micah%grids(3)%region%data(:) = 3.0\n" " micah%grids(3)%region%data(:) = 5.0\n" - " do ji_2 = 1, 5, 1\n" - " grid%data2d(ji_2,i) = 2.0 * grid%data2d(ji_2,i)\n" + " do ji_1 = 1, 5, 1\n" + " grid%data2d(ji_1,i) = 2.0 * grid%data2d(ji_1,i)\n" " enddo\n" " grid%data2d(1:2,i) = 0.0\n" " grid%data2d(:,i) = 3.0\n" " grid%data2d(:,i) = 5.0\n" - " do ji_3 = 1, 5, 1\n" - " grid%data2d(ji_3,i) = 2.0 * grid%data2d(ji_3,i)\n" + " do ji_2 = 1, 5, 1\n" + " grid%data2d(ji_2,i) = 2.0 * grid%data2d(ji_2,i)\n" " enddo\n" " grid%data2d(1:2,i) = 0.0\n" " grid%data2d(1:5,i) = 3.0\n" " grid%data2d(1:5,i) = 5.0\n" + # TODO #1858: replace the following line with the commented-out + # lines below. + " call sub(grid%local%data)\n" + # " do ji_3 = 1, 5, 1\n" + # " grid%local%data(ji_3) = 2.0 * grid%local%data(ji_3)\n" + # " enddo\n" + # " grid%local%data(1:2) = 0.0\n" + # " grid%local%data(:) = 3.0\n" + # " grid%local%data = 5.0\n" " enddo\n" in output) assert Compile(tmpdir).string_compiles(output)