diff --git a/changelog b/changelog index 795583f6b8..6564e95063 100644 --- a/changelog +++ b/changelog @@ -1,4 +1,7 @@ - 46) PR 3033 for #3031 and #3032. Prevent inserting profiling callipers to + 47) PR #2997 for #223. Add support for logical operators in the SymPy + comparisons. + + 46) PR #3033 for #3031 and #3032. Prevent inserting profiling callipers to NEMO functions and any elemental function. Also remove pure attribute when inserting psy_data wrappers. diff --git a/src/psyclone/psyir/backend/sympy_writer.py b/src/psyclone/psyir/backend/sympy_writer.py index b0fef86fcd..a53b602c78 100644 --- a/src/psyclone/psyir/backend/sympy_writer.py +++ b/src/psyclone/psyir/backend/sympy_writer.py @@ -39,7 +39,7 @@ ''' import keyword -from typing import Dict, List, Optional +from typing import Iterable, Optional, Union import sympy from sympy.parsing.sympy_parser import parse_expr @@ -50,7 +50,9 @@ from psyclone.psyir.backend.visitor import VisitorError from psyclone.psyir.frontend.sympy_reader import SymPyReader from psyclone.psyir.nodes import ( - Call, DataNode, IntrinsicCall, Node, Range, Reference, StructureReference) + ArrayOfStructuresReference, ArrayReference, BinaryOperation, Call, + DataNode, IntrinsicCall, Literal, Node, + Range, Reference, StructureReference) from psyclone.psyir.symbols import ( ArrayType, DataSymbol, RoutineSymbol, ScalarType, Symbol, SymbolError, SymbolTable, UnresolvedType) @@ -101,7 +103,15 @@ class SymPyWriter(FortranWriter): # A list of all reserved Python keywords (Fortran variables that are the # same as a reserved name must be renamed, otherwise parsing will fail). # This class attribute will get initialised in __init__: - _RESERVED_NAMES = set() + _RESERVED_NAMES: set[str] = set() + + # A mapping of PSyIR's logical binary operations to the required + # SymPy format: + _BINARY_OP_MAPPING: dict[BinaryOperation.Operator, str] = \ + {BinaryOperation.Operator.AND: "And({lhs}, {rhs})", + BinaryOperation.Operator.OR: "Or({lhs}, {rhs})", + BinaryOperation.Operator.EQV: "Equivalent({lhs}, {rhs})", + BinaryOperation.Operator.NEQV: "Xor({lhs}, {rhs})"} def __init__(self): super().__init__() @@ -180,7 +190,7 @@ def __new__(cls, *expressions): :returns: either an instance of SymPyWriter, if no parameter is specified, or a list of SymPy expressions. :rtype: Union[:py:class:`psyclone.psyir.backend.SymPyWriter`, - List[:py:class:`sympy.core.basic.Basic`]] + list[:py:class:`sympy.core.basic.Basic`]] ''' if expressions: @@ -204,11 +214,12 @@ def __getitem__(self, _): "never be called.") # ------------------------------------------------------------------------- - def _create_sympy_array_function(self, - name: str, - sig: Optional[Signature] = None, - num_dims: Optional[List[int]] = None, - is_call: Optional[bool] = False): + def _create_sympy_array_function( + self, + name: str, + sig: Optional[Signature] = None, + num_dims: Optional[list[int]] = None, + is_call: Optional[bool] = False) -> sympy.Function: '''Creates a Function class with the given name to be used for SymPy parsing. This Function overwrites the conversion to string, and will replace the triplicated array indices back to the normal Fortran @@ -217,7 +228,7 @@ def _create_sympy_array_function(self, to the object, so that the SymPyReader can recreate the proper access to a user-defined type. - :param str name: name of the function class to create. + :param name: name of the function class to create. :param sig: the signature of the variable, which is required to convert user defined types back properly. Only defined for user-defined types. @@ -261,7 +272,7 @@ def _create_sympy_array_function(self, @staticmethod def _ndims_for_struct_access(sig: Signature, - sva: SingleVariableAccessInfo) -> List[int]: + sva: SingleVariableAccessInfo) -> list[int]: ''' The same Signature can be accessed with different numbers of indices, e.g. a%b, a%b(1) and a(1)%b. This routine examines all accesses and @@ -323,8 +334,8 @@ def _specialise_array_symbol(sym: Symbol, sva: SingleVariableAccessInfo): return # ------------------------------------------------------------------------- - def _create_type_map(self, list_of_expressions: List[Node], - identical_variables: Optional[Dict[str, str]] = None, + def _create_type_map(self, list_of_expressions: Iterable[Node], + identical_variables: Optional[dict[str, str]] = None, all_variables_positive: Optional[bool] = None): '''This function creates a dictionary mapping each access in any of the expressions to either a SymPy Function (if the reference @@ -380,6 +391,7 @@ def _create_type_map(self, list_of_expressions: List[Node], # conversion). First, add all reserved names so that these names will # automatically be renamed. The symbol table is used later to also # create guaranteed unique names for lower and upper bounds. + # pylint: disable=too-many-locals, too-many-branches self._symbol_table = SymbolTable() for reserved in SymPyWriter._RESERVED_NAMES: self._symbol_table.new_symbol(reserved) @@ -492,17 +504,20 @@ def no_bounds(self) -> str: # ------------------------------------------------------------------------- @property - def type_map(self): + def type_map(self) -> dict[str, Union[sympy.core.symbol.Symbol, + sympy.core.function.Function]]: ''':returns: the mapping of names to SymPy symbols or functions. - :rtype: Dict[str, Union[:py:class:`sympy.core.symbol.Symbol`, - :py:class:`sympy.core.function.Function`]] ''' return self._sympy_type_map # ------------------------------------------------------------------------- - def _to_str(self, list_of_expressions, identical_variables=None, - all_variables_positive=False): + def _to_str( + self, + list_of_expressions: Union[Node, Iterable[Node]], + identical_variables: Optional[dict[str, str]] = None, + all_variables_positive: Optional[bool] = False) -> Union[str, + list[str]]: '''Converts PSyIR expressions to strings. It will replace Fortran- specific expressions with code that can be parsed by SymPy. The argument can either be a single element (in which case a single string @@ -514,22 +529,18 @@ def _to_str(self, list_of_expressions, identical_variables=None, :param identical_variables: which variable names are known to be identical - :type identical_variables: Optional[dict[str, str]] - :param list_of_expressions: the list of expressions which are to be converted into SymPy-parsable strings. - :type list_of_expressions: Union[:py:class:`psyclone.psyir.nodes.Node`, - List[:py:class:`psyclone.psyir.nodes.Node`]] - :param Optional[bool] all_variables_positive: whether or not (the + :param all_variables_positive: whether or not (the default) to assume that all variables are positive definite quantities. :returns: the converted strings(s). - :rtype: Union[str, List[str]] ''' - is_list = isinstance(list_of_expressions, (tuple, list)) - if not is_list: + is_list = True + if isinstance(list_of_expressions, Node): + is_list = False list_of_expressions = [list_of_expressions] # Create the type map in `self._sympy_type_map`, which is required @@ -549,8 +560,13 @@ def _to_str(self, list_of_expressions, identical_variables=None, return expression_str_list # ------------------------------------------------------------------------- - def __call__(self, list_of_expressions, identical_variables=None, - all_variables_positive=False): + def __call__( + self, + list_of_expressions: Union[Node, list[Node]], + identical_variables: Optional[dict[str, str]] = None, + all_variables_positive: Optional[bool] = False) \ + -> Union[sympy.core.basic.Basic, + list[sympy.core.basic.Basic]]: ''' This function takes a list of PSyIR expressions, and converts them all into Sympy expressions using the SymPy parser. @@ -565,11 +581,8 @@ def __call__(self, list_of_expressions, identical_variables=None, :param list_of_expressions: the list of expressions which are to be converted into SymPy-parsable strings. - :type list_of_expressions: list of - :py:class:`psyclone.psyir.nodes.Node` :param identical_variables: which variable names are known to be identical - :type identical_variables: Optional[dict[str, str]] :param Optional[bool] all_variables_positive: whether or not (the default) to assume that all variables are positive definite quantities. @@ -577,8 +590,6 @@ def __call__(self, list_of_expressions, identical_variables=None, :returns: a 2-tuple consisting of the the converted PSyIR expressions, followed by a dictionary mapping the symbol names to SymPy Symbols. - :rtype: Union[:py:class:`sympy.core.basic.Basic`, - List[:py:class:`sympy.core.basic.Basic`]] :raises VisitorError: if an invalid SymPy expression is found. :raises TypeError: if the identical_variables parameter is not @@ -595,9 +606,11 @@ def __call__(self, list_of_expressions, identical_variables=None, raise TypeError("Dictionary identical_variables " "contains a non-string key or value.") - is_list = isinstance(list_of_expressions, (tuple, list)) - if not is_list: + is_list = True + if isinstance(list_of_expressions, Node): + is_list = False list_of_expressions = [list_of_expressions] + expression_str_list = self._to_str( list_of_expressions, identical_variables=identical_variables, all_variables_positive=all_variables_positive) @@ -617,37 +630,37 @@ def __call__(self, list_of_expressions, identical_variables=None, return result[0] # ------------------------------------------------------------------------- - def arrayreference_node(self, node): + def arrayreference_node(self, node: ArrayReference) -> str: '''The implementation of the method handling a ArrayOfStructureReference is generic enough to also handle non-structure arrays. So just use it. :param node: a ArrayReference PSyIR node. - :type node: :py:class:`psyclone.psyir.nodes.ArrayReference` :returns: the code as string. - :rtype: str ''' return self.arrayofstructuresreference_node(node) # ------------------------------------------------------------------------- - def structurereference_node(self, node): + def structurereference_node(self, node: StructureReference) -> str: '''The implementation of the method handling a ArrayOfStructureReference is generic enough to also handle non-arrays. So just use it. :param node: a StructureReference PSyIR node. - :type node: :py:class:`psyclone.psyir.nodes.StructureReference` :returns: the code as string. - :rtype: str ''' return self.arrayofstructuresreference_node(node) # ------------------------------------------------------------------------- - def arrayofstructuresreference_node(self, node: StructureReference) -> str: + def arrayofstructuresreference_node( + self, + node: Union[ArrayOfStructuresReference, + ArrayReference, + StructureReference]) -> str: ''' This handles ArrayOfStructureReferences (and also simple StructureReferences). @@ -660,7 +673,7 @@ def arrayofstructuresreference_node(self, node: StructureReference) -> str: sig, indices = node.get_signature_and_indices() all_dims = [] - for i, name in enumerate(sig): + for i, _ in enumerate(sig): if indices[i]: for index in indices[i]: all_dims.append(index) @@ -681,7 +694,7 @@ def arrayofstructuresreference_node(self, node: StructureReference) -> str: return unique_name # ------------------------------------------------------------------------- - def literal_node(self, node): + def literal_node(self, node: Literal) -> str: '''This method is called when a Literal instance is found in the PSyIR tree. For SymPy we need to handle booleans (which are expected to be capitalised: True). Real values work by just ignoring any precision @@ -689,10 +702,8 @@ def literal_node(self, node): and will raise an exception. :param node: a Literal PSyIR node. - :type node: :py:class:`psyclone.psyir.nodes.Literal` :returns: the SymPy representation for the literal. - :rtype: str :raises TypeError: if a character constant is found, which is not supported with SymPy. @@ -735,17 +746,16 @@ def call_node(self, node: Call) -> str: indices_str = self.gen_indices(node.arguments) return f"{unique_name}({','.join(indices_str)})" - def intrinsiccall_node(self, node): + # ------------------------------------------------------------------------- + def intrinsiccall_node(self, node: IntrinsicCall) -> str: ''' This method is called when an IntrinsicCall instance is found in the PSyIR tree. The Sympy backend will use the exact sympy name for some math intrinsics (listed in _intrinsic_to_str) and will remove named arguments. :param node: an IntrinsicCall PSyIR node. - :type node: :py:class:`psyclone.psyir.nodes.IntrinsicCall` :returns: the SymPy representation for the Intrinsic. - :rtype: str ''' # Sympy does not support argument names, remove them for now @@ -774,7 +784,7 @@ def intrinsiccall_node(self, node): return super().call_node(node) # ------------------------------------------------------------------------- - def reference_node(self, node): + def reference_node(self, node: Reference) -> str: '''This method is called when a Reference instance is found in the PSyIR tree. It handles the case that this normal reference might be an array expression, which in the SymPy writer needs to have @@ -782,10 +792,8 @@ def reference_node(self, node): ``a`` to ``a(sympy_no_bounds, sympy_no_bounds, 1)``. :param node: a Reference PSyIR node. - :type node: :py:class:`psyclone.psyir.nodes.Reference` :returns: the text representation of this reference. - :rtype: str ''' # Support renaming a symbol (e.g. if it is a reserved Python name). @@ -813,7 +821,26 @@ def reference_node(self, node): f"{','.join(result)}{self.array_parenthesis[1]}") # ------------------------------------------------------------------------ - def gen_indices(self, indices, var_name=None): + def binaryoperation_node(self, node: BinaryOperation) -> str: + '''This function converts logical binary operations into + SymPy format. Non-logical binary operations have the same + representation otherwise, so it calls the base class. + + :param node: a Reference PSyIR BinaryOperation. + + ''' + if node.operator in self._BINARY_OP_MAPPING: + lhs = self._visit(node.children[0]) + rhs = self._visit(node.children[1]) + return self._BINARY_OP_MAPPING[node.operator].format(rhs=rhs, + lhs=lhs) + + return super().binaryoperation_node(node) + + # ------------------------------------------------------------------------ + def gen_indices(self, + indices: Iterable[Node], + var_name: Optional[str] = None): '''Given a list of PSyIR nodes representing the dimensions of an array, return a list of strings representing those array dimensions. This is used both for array references and array declarations. Note @@ -822,12 +849,10 @@ def gen_indices(self, indices, var_name=None): each array index into three parameters to support array expressions. :param indices: list of PSyIR nodes. - :type indices: List[:py:class:`psyclone.psyir.symbols.Node`] - :param str var_name: name of the variable for which the dimensions + :param var_name: name of the variable for which the dimensions are created. Not used in this implementation. :returns: the Fortran representation of the dimensions. - :rtype: List[str] :raises NotImplementedError: if the format of the dimension is not supported. @@ -860,16 +885,14 @@ def gen_indices(self, indices, var_name=None): return dims # ------------------------------------------------------------------------- - def range_node(self, node): + def range_node(self, node: Range) -> str: '''This method is called when a Range instance is found in the PSyIR tree. This implementation converts a range into three parameters for the corresponding SymPy function. :param node: a Range PSyIR node. - :type node: :py:class:`psyclone.psyir.nodes.Range` :returns: the Fortran code as a string. - :rtype: str ''' if node.parent and node.parent.is_lower_bound( diff --git a/src/psyclone/psyir/frontend/sympy_reader.py b/src/psyclone/psyir/frontend/sympy_reader.py index fc3cd8520b..db8e3f3198 100644 --- a/src/psyclone/psyir/frontend/sympy_reader.py +++ b/src/psyclone/psyir/frontend/sympy_reader.py @@ -37,10 +37,36 @@ '''PSyIR frontend to convert a SymPy expression to PSyIR ''' +from sympy.printing.printer import Printer from psyclone.psyir.frontend.fortran import FortranReader +# pylint: disable=invalid-name +class FortranPrinter(Printer): + '''Specialise the SymPy Printer to convert logical operators back to + Fortran format. While SymPy has a Fortran printer (fcode), it does not + handle e.g. Fortran Array expressions (a(2:5)), so we specialise the + generic SymPy Printer and handle the necessary conversions.''' + + def _print_And(self, expr): + '''Called when converting an AND expression.''' + return f"({'.AND.' .join(self._print(i) for i in expr.args)})" + + def _print_Or(self, expr): + '''Called when converting an OR expression.''' + return f"({'.OR.' .join(self._print(i) for i in expr.args)})" + + def _print_Equivalent(self, expr): + '''Called when converting an EQUIVALENT expression.''' + return f"({'.EQV.' .join(self._print(i) for i in expr.args)})" + + def _print_Xor(self, expr): + '''Called when converting an XOR expression, which in Fortran + is NEQV.''' + return f"({'.NEQV.' .join(self._print(i) for i in expr.args)})" + + class SymPyReader(): '''This class converts a SymPy expression, that was created by the SymPyWriter, back to PSyIR. It basically allows to use SymPy to modify @@ -123,7 +149,9 @@ def psyir_from_expression(self, sympy_expr, symbol_table): ''' # Convert the new sympy expression to PSyIR reader = FortranReader() - return reader.psyir_from_expression(str(sympy_expr), symbol_table) + fp = FortranPrinter() + return reader.psyir_from_expression(fp.doprint(sympy_expr), + symbol_table) # ------------------------------------------------------------------------- # pylint: disable=no-self-argument, too-many-branches diff --git a/src/psyclone/tests/core/symbolic_maths_test.py b/src/psyclone/tests/core/symbolic_maths_test.py index 7b7ce82a1b..4b3cb3be91 100644 --- a/src/psyclone/tests/core/symbolic_maths_test.py +++ b/src/psyclone/tests/core/symbolic_maths_test.py @@ -602,3 +602,73 @@ def test_symbolic_maths_array_and_array_index(fortran_reader): assert sym_maths.equal(assigns[0].rhs, assigns[0].rhs) assert sym_maths.equal(assigns[1].rhs, assigns[1].rhs) + + +@pytest.mark.parametrize( + "expressions", + [(".false. .and. .false.", "False"), + (".false. .and. .true.", "False"), + (".true. .and. .false.", "False"), + (".true. .and. .true.", "True"), + (".false. .or. .false.", "False"), + (".false. .or. .true.", "True"), + (".true. .or. .false.", "True"), + (".true. .or. .true.", "True"), + (".false. .eqv. .false.", "True"), + (".false. .eqv. .true.", "False"), + (".true. .eqv. .false.", "False"), + (".true. .eqv. .true.", "True"), + (".false. .neqv. .false.", "False"), + (".false. .neqv. .true.", "True"), + (".true. .neqv. .false.", "True"), + (".true. .neqv. .true.", "False"), + (" .false. .and. ((3 -2 + 4 - 5) .eq. 0 .and. .false.)", False), + ]) +def test_sym_writer_boolean_expr(fortran_reader, expressions): + '''Test that booleans are written in the way that SymPy accepts. + ''' + # A dummy program to easily create the PSyIR for the + # expressions we need. We just take the RHS of the assignments + source = f'''program test_prog + logical :: bool_expr + bool_expr = {expressions[0]} + bool_expr = {expressions[1]} + end program test_prog ''' + + psyir = fortran_reader.psyir_from_source(source) + lit0 = psyir.children[0].children[0].rhs + lit1 = psyir.children[0].children[1].rhs + sympy_writer = SymPyWriter() + + sympy_expr = sympy_writer(lit0) + assert sympy_expr == sympy_writer(lit1) + + +@pytest.mark.parametrize( + "expressions", + [(".true. .and. .false.", False), + (".true. .and. .true.", True), + (".false. .or. .true.", True), + ("3 .eq. 3", True), + (" ((3 -2 + 4 - 5) .eq. 0 .and. .false.) .or. .true.", True), + (" ((3 -2 + 4 - 5) .eq. 0 .and. .true.)", True), + (" (3 -2 + 4 - 5) .eq. 0 .and. .false. .and. .true.", False), + (" ((3 -2 + 4 - 5) .eq. 0 .and. .false.) .and. .true.", False), + (" .false. .and. ((3 -2 + 4 - 5) .eq. 0 .and. .false.)", False), + (" (((3 -2 + 4 - 5) .eq. 0) .and. .false.)", False), + ]) +def test_sym_writer_boolean_expr_add_test(fortran_reader, expressions): + '''Test that booleans are written in the way that SymPy accepts. + ''' + # A dummy program to easily create the PSyIR for the + # expressions we need. We just take the RHS of the assignments + source = f'''program test_prog + logical :: bool_expr + bool_expr = {expressions[0]} + end program test_prog ''' + + psyir = fortran_reader.psyir_from_source(source) + lit = psyir.children[0].children[0].rhs + sympy_writer = SymPyWriter() + sympy_expr = sympy_writer(lit) + assert sympy_expr == expressions[1] diff --git a/src/psyclone/tests/psyir/backend/sympy_writer_test.py b/src/psyclone/tests/psyir/backend/sympy_writer_test.py index 60a574aa82..9dedf452dd 100644 --- a/src/psyclone/tests/psyir/backend/sympy_writer_test.py +++ b/src/psyclone/tests/psyir/backend/sympy_writer_test.py @@ -512,6 +512,37 @@ def test_sympy_writer_user_types(fortran_reader, fortran_writer, assert fortran_writer(new_psyir) == fortran_expr +@pytest.mark.parametrize("fortran_expr,sympy_str", + [("a .and. b", "And(a, b)"), + ("a .or. b", "Or(a, b)"), + ("a .eqv. b", "Equivalent(a, b)"), + ("a .neqv. b", "Xor(a, b)"), + ]) +def test_sympy_writer_logicals(fortran_reader, fortran_writer, + fortran_expr, sympy_str): + '''Test handling of user-defined types, e.g. conversion of + ``a(i)%b(j)`` to ``a_b(i,i,1,j,j,1)``. Each Fortran expression + ``fortran_expr`` is first converted to a string ``sympy_str`` to be + parsed by SymPy. The sympy expression is then converted back to PSyIR. + This string must be the same as the original ``fortran_expr``. + + ''' + source = f'''program test_prog + logical :: a, b, x + x = {fortran_expr} + end program test_prog''' + + psyir = fortran_reader.psyir_from_source(source) + # Get the actual fortran expression requested: + psyir_expr = psyir.children[0].children[0].rhs + + # Convert the PSyIR to a SymPy string: + sympy_writer = SymPyWriter() + out = sympy_writer._to_str([psyir_expr]) + # Make sure we get the expected string as output: + assert out[0] == sympy_str + + @pytest.mark.parametrize("expression", ["def", "if", "raise", "del", "import", "return", "elif", "in", "try", "and", "else", "is", "while", diff --git a/src/psyclone/tests/psyir/frontend/sympy_reader_test.py b/src/psyclone/tests/psyir/frontend/sympy_reader_test.py index a792d89f22..efb948eecb 100644 --- a/src/psyclone/tests/psyir/frontend/sympy_reader_test.py +++ b/src/psyclone/tests/psyir/frontend/sympy_reader_test.py @@ -73,6 +73,18 @@ def test_sympy_reader_constructor(): ("b(:5:2)", "b(:5:2)"), ("b(2:5:1)", "b(2:5)"), ("b(2:5:2)", "b(2:5:2)"), + ("i .and. j", "i .AND. j"), + ("i .and. j .and. k", + "i .AND. j .AND. k"), + ("i .or. j", "i .OR. j"), + # Precedence requires the () + ("i .and. (i .or. j)", + "i .AND. (i .OR. j)"), + # Precedence rules discard the () + ("i .or. (i .and. j)", + "i .OR. i .AND. j"), + ("i .eqv. j", "i .EQV. j"), + ("i .neqv. j", "i .NEQV. j"), ]) def test_sympy_psyir_from_expression(fortran_reader, fortran_writer, expressions): @@ -87,7 +99,7 @@ def test_sympy_psyir_from_expression(fortran_reader, fortran_writer, ''' source = f'''program test_prog use my_mod - integer :: i, j + integer :: i, j, k integer :: a, b(10), c(10, 10) type(my_mod_type) :: d, e(10) x = {expressions[0]}