# Source code for psyclone.psyir.frontend.sympy_reader

```
# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2023-2024, Science and Technology Facilities Council
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
# Author: J. Henrichs, Bureau of Meteorology
'''PSyIR frontend to convert a SymPy expression to PSyIR
'''
from psyclone.psyir.frontend.fortran import FortranReader
[docs]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
PSyIR expressions:
1. The SymPyWriter converts the Fortran expression to SymPy
2. SymPy is used to manipulate these mathematical expressions
3. The SymPyReader is used to convert these SymPy expressions back
into PSyIR (from which source code can be recreated)
Most SymPy expressions can be parsed as Fortran expression immediately,
but the big exception to this are Fortran Arrays which must be able to
support array expressions like ``a(1:5:2)``. As outlined in the
SymPyWriter, each array is represented as an UndefinedFunction in SymPy,
and each each dimension of the Fortran array will provide three
arguments to this function: the start, stop, and step value. So the
expression above will be converted to ``a(1,5,2)``, and ``a(7)`` will
be represented as ``a(7:7:1)``. If the start- or stop-expression is
not specified, e.g. ``a(:)``, the SymPy writer will use two special
symbols ``sympy_lower`` and ``sympy_upper``, e.g. the above expression
will become ``a(sympy_lower:sympy_upper:1)``. The SymPyWriter will
change the name if required in case of a name clash (i.e. if the user
declared a variable called ``sympy_lower`` etc).
The task of the SymPy reader is to convert these expressions back to
the original PSyIR representation. For example, the SymPy expressions
``a(i,j,k)`` will be written as ``a(i:j:k)``. This conversion is done
by the function ``print_fortran_array`` by combining each 3-tuple of
arguments back to the corresponding Fortran representation.
The SymPyWriter sets the ``_sympystr`` attribute of the SymPy
UndefinedFunction it creates to ``print_fortran_array``. The ``_sympystr``
method is automatically called by SymPy when converting an expression
into a string.
In order to achieve this, the SymPyReader must know the names used for
the lower- and upper-bounds. The constructor takes a SymPyWriter as
argument in order to get the name of these bounds. It is important that
the SymPyWriter provided here is the one that was used to create the SymPy
expressions in the first place.
:param sympy_writer: the SymPyWriter that was used to create the SymPy
expressions.
:type sympy_writer: :py:class:`psyclone.psyir.backend.SymPyWriter`
'''
# The default values for the bounds, they will be changed to the
# correct values in the constructor.
_lower_bound = "sympy_lower"
_upper_bound = "sympy_upper"
def __init__(self, sympy_writer):
SymPyReader._lower_bound = sympy_writer.lower_bound
SymPyReader._upper_bound = sympy_writer.upper_bound
# -------------------------------------------------------------------------
[docs] def psyir_from_expression(self, sympy_expr, symbol_table):
'''This function converts a SymPy expression back into PSyIR. It
converts the SymPy expression into a string, which is then parsed
by the FortranReader. It relies on the ``print_fortran_array``
function to convert the function arguments back to Fortran array
expressions. This function will be called by SymPy when converting the
functions that represent array indices into a string, see
:py:class:`psyclone.psyir.backend.SymPyWriter._create_type_map`,
where this function is defined to be called for string conversion.
:param sympy_expr: the original SymPy expression.
:type sympy_expr: :py:class:`sympy.core.basic.Basic`
:param symbol_table: the symbol table required for parsing, it
should be the table from which the original SymPy expression
was created from (i.e. contain all the required symbols in the
SymPy expression).
:type symbol_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
:returns: the PSyIR representation of the SymPy expression.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
# Convert the new sympy expression to PSyIR
reader = FortranReader()
return reader.psyir_from_expression(str(sympy_expr), symbol_table)
# -------------------------------------------------------------------------
# pylint: disable=no-self-argument, too-many-branches
[docs] def print_fortran_array(function, printer):
'''A custom print function to convert a modified Fortran array access
back to standard Fortran. This function is set as ``_sympystr_`` method
of the SymPy functions created in the SymPyWriter (see
``_create_type_map`` method of the SymPyWriter), so it will be called
by SymPy to convert this function to a string, with the function to
convert being the first argument! This function converts
the three values that each index is converted to back into the Fortran
array notation. It uses the class variables
``SymPyReader._lower_bound`` and ``SymPyReader._upper_bound`` as the
names that were used when the SymPy expressions were created in order
to convert array expressions correctly back.
:param function: this function is called from a SymPy Function class,
therefore the first argument is a SymPy Function instance (and NOT
a SymPyReader) instance.
:type function: :py:class:`sympy.core.function.Function`
:param printer: the SymPy writer base class.
:type printer: :py:class:`sympy.printing.str.StrPrinter`
:returns: the string representation of this array access.
:rtype: str
'''
# pylint: disable=protected-access, no-member
args = [printer._print(i) for i in function.args]
name = function.__class__.__name__
lower_b = SymPyReader._lower_bound
upper_b = SymPyReader._upper_bound
# Analyse each triple of parameters, and add the corresponding
# converted index (or array expression) to new_args:
new_args = []
for i in range(0, len(args), 3):
if args[i] == args[i+1] and args[i+2] == "1":
# a(i,i,1) --> a(i)
new_args.append(args[i])
elif args[i] == lower_b:
if args[i+1] == upper_b and args[i+2] == "1":
# a(lower_b, upper_b, 1) --> a(:)
new_args.append(":")
else:
if args[i+2] == "1":
# a(lower_b, i, 1) --> a(:i)
new_args.append(f":{args[i+1]}")
else:
# a(lower_b, i, j) --> a(:i:j)
new_args.append(f":{args[i+1]}:{args[i+2]}")
elif args[i+1] == upper_b:
if args[i+2] == "1":
# a(i, upper_b, 1) --> a(i:)
new_args.append(f"{args[i]}:")
else:
# a(i, upper_b, j) --> a(i::j)
new_args.append(f"{args[i]}::{args[i+2]}")
else:
if args[i+2] == "1":
# a(i,j,1) --> a(i:j)
new_args.append(f"{args[i]}:{args[i+1]}")
else:
# a(i,j,k) --> a(i:j:k)
new_args.append(f"{args[i]}:{args[i+1]}:"
f"{args[i+2]}")
if function._sig is None:
# It's not a user defined type, just create the array access:
return f"{name}({','.join(new_args)})"
# It is a user defined type. Re-assemble the original call by
# putting the corresponding indices to the individual members,
# based on the information of the stored signature and number
# of array indices for each member:
result = []
# This points at the next index to use from new_args, which
# contains the indices converted back into Fortran:
index_cursor = 0
for i, member in enumerate(function._sig):
# Get the number of indices this member had:
num_dims = function._num_dims[i]
indx = []
for i in range(num_dims):
indx.append(new_args[index_cursor])
index_cursor += 1
if indx:
result.append(f"{member}({','.join(indx)})")
else:
result.append(member)
return "%".join(result)
```