diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 898bff4d..dea25569 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -84,6 +84,9 @@ jobs: # fparser should work even under limited terminal conditions so set # LC_ALL (this is only relevant for versions before Python 3.7). LC_ALL=POSIX pytest --cov=fparser --cov-report=xml src/fparser + - name: Test examples + run: | + make -C example test - name: Upload coverage to Codecov with GitHub Action uses: codecov/codecov-action@v4 env: diff --git a/.gitignore b/.gitignore index 61cac378..9e94c00f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *.pyc *~ +src/**/*.mod *.log _build/ htmlcov/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e0af643..155de976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,25 @@ Modifications by (in alphabetical order): * A. R. Porter, Science & Technology Facilities Council, UK * B. Reuter, ECMWF, UK * S. Siso, Science & Technology Facilities Council, UK +* M. Schreiber, Universite Grenoble Alpes, France * J. Tiira, University of Helsinki, Finland * P. Vitt, University of Siegen, Germany * A. Voysey, UK Met Office +28/11/2024 PR #455 for #454. Fixes a few tests to use the tmpdir fixture + rather than writing directly to /tmp. + +## Release 0.2.0 (26/11/2024) ## + +25/11/2024 PR #453 extension of base node types to allow the parse tree to be + deepcopied and pickled. + +14/10/2024 PR #451 for #320. Adds an extension to Fortran2003 to support non-standard STOP + expressions and adds support for them in 2008. + +11/10/2024 PR #450 for #448. Adds an example script for removing all protected/private + attributes from a parse tree. + 15/07/2024 PR #438 for #437. Fix type guard statement bug. 24/04/2024 PR #444 for #443. Adds an option to the reader to handle code diff --git a/README.md b/README.md index 6612d749..99847cb1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # News # + * 26/11/2024 Version 0.2.0 released (support for statements behind OMP sentinels, non-standard + STOP and bug fix for type-guard statements). * 31/01/2024 Version 0.1.4 released (resolves issues with WHERE statements and intrinsics). * 18/09/2023 Version 0.1.3 released (resolves issues with support for DO CONCURRENT). * 19/06/2023 Version 0.1.2 released (bug fix for handling of INCLUDE files). diff --git a/doc/source/conf.py b/doc/source/conf.py index 30e46ff3..04b36111 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -10,7 +10,7 @@ project = 'fparser' copyright = '2017-2024, Science and Technology Facilities Council' -author = 'Andrew Porter, Rupert Ford, Balthasar Reuter and Pearu Peterson' +author = 'Andrew Porter, Rupert Ford, Balthasar Reuter, Joerg Henrichs and Pearu Peterson' version = fparser._get_version() release = fparser._get_version() @@ -55,3 +55,26 @@ # Generate the Doxygen documentation subprocess.call('cd ..; doxygen doxygen.config', shell=True) + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, +# documentclass [howto/manual]). +latex_documents = [ + ('index', 'fparser.tex', 'fparser Documentation', + 'Andrew Porter, Rupert Ford, Balthasar Reuter, \\\\ ' + 'Joerg Henrichs and Pearu Peterson', 'manual'), +] diff --git a/doc/source/developers_guide.rst b/doc/source/developers_guide.rst index c58cf220..44218df2 100644 --- a/doc/source/developers_guide.rst +++ b/doc/source/developers_guide.rst @@ -403,7 +403,7 @@ and for Fortran2008 it is R202 program-unit is main-program or external-subprogram or module - or submodule + or submodule or block-data Therefore to implement the Fortran2008 version of this class, the @@ -1009,7 +1009,7 @@ f2003_create -- Sets-up the class hierarchy for the Fortran2003 parser. f2003_parser `Fortran2003.Program` Sets-up the class hierarchy for the Fortran2003 parser and returns the - top-level Program object. + top-level Program object. clear_symbol_table -- Removes all stored symbol tables. fake_symbol_table -- Creates a fake scoping region and associated symbol table. diff --git a/doc/source/examples.rst b/doc/source/examples.rst new file mode 100644 index 00000000..1c9e4519 --- /dev/null +++ b/doc/source/examples.rst @@ -0,0 +1,149 @@ +.. -*- rest -*- + +.. + Copyright (c) 2024 Science and Technology Facilities Council. + + All rights reserved. + + Modifications made as part of the fparser project are distributed + under the following license: + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. 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. + + 3. 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. + + +.. _examples: + +Examples +======== + +The distribution comes with a set of examples showing how fparser can +be used. At the same time, some of these examples are actually useful tools +that are used in other projects. + + +fparser2_f2008.py +^^^^^^^^^^^^^^^^^ +This is a very small example code that shows how a Fortran code, given as +a string, is parsed, and then converted back into Fortran. It just prints +out the re-created Fortran source code. + + +create_dependencies.py +^^^^^^^^^^^^^^^^^^^^^^ +This file analyses the dependencies between a set of Fortran files, based on +the ``Use`` statements in each file. It assumes that the module name in the +``use`` statement corresponds to the name of the file (adding one of +.F90/.f90/.x90). Only files in the current directory will be tested, so +external dependencies will not be listed. Its output +is in a format that can be immediately used in a Makefile:: + + ../create_dependencies.py *.f90 + b.o: a.o + c.o: a.o b.o + + +Ignoring error handling, the simplified main part of this code +that is related to fparser is:: + + reader = FortranFileReader(filename) + parser = ParserFactory().create(std="f2003") + parse_tree = parser(reader) + + # Collect all used modules in a list + all_use = set() + for node in walk(parse_tree, Use_Stmt): + use_name = str(node.items[2]) + # A more sophisticated mapping could be used here. + # But for now just assume that the name in the use statement + # with an added ".o" is the required object file: + obj_dependency = use_name + ".o" + all_use.add(use_name) + # Now ``all_use`` contains all .o files that ``filename`` depends on + +The rest of the example is related to creating the proper format for +a Makefile. + +.. note:: It would be straight-forward to loop over all files twice, first + to collect the module names provided by each file, then use this + information + + +make_public.py +^^^^^^^^^^^^^^ +This example removes all ``private`` and ``protected`` attributes in any +declaration. In general these attributes are important and should +obviously not be removed in order to give the compiler more information +about intended use of the variables. But `PSyclone +`_ offers a feature called +Kernel Extraction, which automatically writes all variables read and written +in a code section to a file. It also then creates a stand-alone driver program +that will read this file, execute the kernel, and compare the results with +the original results. + +Since PSyclone will follow the call tree, the code must be able to read even +variables declared as ``private`` (to write them into the output file), and +a driver program must be able to modify ``private`` and ``protected`` +variables in modules. If the driver creation is used, the +`Fab `_ based build system will +remove all ``private`` and ``protected`` attributes in a separate build phase, +so that the kernel extraction and driver creation works as expected. + +A short example, which shows how a ``Access_Stmt`` like ``private :: a`` is +removed:: + + for node in walk(parse_tree, Access_Stmt): + if node.items[0] == "PRIVATE": + # Find the node in the parent, and remove it: + node.parent.children.remove(node) + + +Modifying some of the fparser data structures can be more difficult, since +they are often based on Python tuples, which cannot be modified. The following +example from ``make_public.py`` shows how the middle element of a three-element +tuple is replaced with None:: + + type_decl.items = (type_decl.items[0], None, type_decl.items[2]) + + +split_file.py +~~~~~~~~~~~~~ +This script splits one Fortran source file into several files, each containing +one top level module, subroutine, function or program. Each file uses the name +of the program unit (module-, subroutine-, function-, program name). The +extension will be ``.F90`` if there are preprocessor directives in the file, +and ``.f90`` otherwise. + +Additionally, ``split_file.py`` will create a Makefile to build either the +binary (if a program is found in the file), or all object files. If any of +the environment variables ``F90``, ``F90FLAGS``, and ``LDFLAGS`` are set at +run time of the script, it will use these values as default values in the +makefile. But by setting these environment variables when running ``make``, +these defaults can always be overwritten. The Makefile also has a ``clean`` +target, which will remove all ``.mod``, object, and the program file (if +available). It uses the ``create_dependencies.py`` script to add the +required dependencies to the Makefile. diff --git a/doc/source/fparser2.rst b/doc/source/fparser2.rst index 3adf1d33..34e1de45 100644 --- a/doc/source/fparser2.rst +++ b/doc/source/fparser2.rst @@ -374,6 +374,19 @@ This extension is supported by (at least) the Gnu, Intel and Cray compilers but is not a part of any Fortran standard. More details can be found at https://gcc.gnu.org/onlinedocs/gfortran/CONVERT-specifier.html +Extended arguments for STOP ++++++++++++++++++++++++++++ + +Many compilers support extended arguments for the STOP statement before Fortran 2008. +Examples are negative numbers, and string operations:: + + STOP -1 + STOP str1 // str2 + +This extension will accept these expressions in Fortran 2003. Note that the +Fortran 2008 standard changes the definition of the stop code to accept even +more flexible expressions. + Classes ------- diff --git a/doc/source/index.rst b/doc/source/index.rst index fc638c0e..231e57bc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -79,5 +79,6 @@ Welcome to fparser's documentation! introduction fparser fparser2 + examples developers_guide reference_guide diff --git a/doc/source/license.rst b/doc/source/license.rst index 9789706f..db825cd5 100644 --- a/doc/source/license.rst +++ b/doc/source/license.rst @@ -5,8 +5,9 @@ License ======= | Modified work Copyright (c) 2017-2021 Science and Technology Facilities Council -| Authors: **Andrew Porter** and **Rupert Ford**, STFC Daresbury Laboratory, and -| **Balthasar Reuter**, ECMWF +| Authors: **Andrew Porter** and **Rupert Ford**, STFC Daresbury Laboratory, +| **Balthasar Reuter**, ECMWF, and +| **Joerg Henrichs**, Bureau of Meteorology | Original work Copyright (c) 1999-2008 **Pearu Peterson** | All rights reserved. diff --git a/example/Makefile b/example/Makefile index 924915e6..b801388a 100644 --- a/example/Makefile +++ b/example/Makefile @@ -33,17 +33,25 @@ # ------------------------------------------------------------------------------ # Author: J. Henrichs, Bureau of Meteorology -.PHONY: test create_dependency fparser2_f2008 +# A simple Makefile driver to test the various examples. -test: create_dependencies split_file fparser2_f2008 + +.PHONY: test create_dependency fparser2_f2008 make_public split_file + +test: create_dependencies fparser2_f2008 make_public split_file create_dependencies: - (cd test_files/create_dependencies; ../../create_dependencies.py *.f90) + (cd test_files/create_dependencies; \ + ../../create_dependencies.py *.f90 | diff correct_dependencies -) + +fparser2_f2008: + python ./fparser2_f2008.py + +make_public: + ./make_public.py test_files/make_public.f90 | diff test_files/make_public_correct.f90 - split_file: (cd test_files/split_file; \ rm -f Makefile func.f90 mod1.f90 sub.f90 test_prog.f90; \ - ../../split_file.py test.f90) - -fparser2_f2008: - python ./fparser2_f2008.py \ No newline at end of file + ../../split_file.py test.f90; \ + cat mod1.f90 sub.f90 func.f90 test_prog.f90 | diff -B test.f90 -) diff --git a/example/make_public.py b/example/make_public.py new file mode 100755 index 00000000..562c9857 --- /dev/null +++ b/example/make_public.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python + +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 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: Joerg Henrichs, Bureau of Meteorology + +"""This file contains a script that will remove any private or protected +declaration in a Fortran file. This is used by PSyclone's kernel extraction +and driver creation feature, see +https://psyclone.readthedocs.io/en/latest/psyke.html. +for details. + +Usage: make_public.py file1.f90 +""" + +import sys + +from fparser.common.readfortran import FortranFileReader +from fparser.two.Fortran2003 import ( + Access_Stmt, + Access_Spec, + Attr_Spec, + Binding_Private_Stmt, + Private_Components_Stmt, + Protected_Stmt, +) +from fparser.two.parser import ParserFactory +from fparser.two.utils import walk + + +# ----------------------------------------------------------------------------- +def remove_private(filename): + """Simple function that removes all private and protected declarations. + :param str filename: the file in which to remove private and protected + """ + reader = FortranFileReader(filename) + parser = ParserFactory().create(std="f2008") + parse_tree = parser(reader) + # A useful print to see the actual rules + # print(repr(parse_tree)) + + # Loop over all access and protected statements. Note that a + # `protected_stmt` is not an access statement, so it needs to + # be listed additionally: + for node in walk( + parse_tree, + (Access_Stmt, Protected_Stmt, Private_Components_Stmt, Binding_Private_Stmt), + ): + # A Private_Components_Stms has no items: + if isinstance( + node, (Binding_Private_Stmt, Private_Components_Stmt) + ) or node.items[0] in ["PRIVATE", "PROTECTED"]: + # Find the node in the parent, and remove it: + node.parent.children.remove(node) + + for node in walk(parse_tree, Access_Spec): + if str(node) == "PRIVATE": + node.string = "PUBLIC" + + all_nodes = list(walk(parse_tree, Attr_Spec)) + for node in all_nodes: + if str(node) == "PROTECTED": + # This is a tuple, so we can't simply remove the attribute + node.parent.items = tuple(i for i in node.parent.items if i is not node) + # If all items in the Attr_Spec are removed, we need to replace the + # Attr_Spec in the parent-parent (Type_Declaration) with None, + # otherwise fparser will create e.g. `real, :: a` + if len(node.parent.items) == 0: + # Again all tuples, which we can't modify, so we need to + # recreate the tuple but replace the attr_spec with None + type_decl = node.parent.parent + type_decl.items = (type_decl.items[0], None, type_decl.items[2]) + + return parse_tree + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + user_filename = sys.argv[1] + modified_parse_tree = remove_private(user_filename) + print(modified_parse_tree) diff --git a/example/split_file.py b/example/split_file.py index 7bf075a1..97babd22 100755 --- a/example/split_file.py +++ b/example/split_file.py @@ -192,6 +192,7 @@ def main(): sys.exit(-1) with open(filename, mode="w", encoding="utf-8") as f_out: f_out.write(str(unit)) + f_out.write("\n") all_filenames.append(filename) all_objs.append(f"{unit_name}.o") diff --git a/example/test_files/create_dependencies/correct_dependencies b/example/test_files/create_dependencies/correct_dependencies new file mode 100644 index 00000000..3b14731f --- /dev/null +++ b/example/test_files/create_dependencies/correct_dependencies @@ -0,0 +1,2 @@ +b.o: a.o +c.o: a.o b.o diff --git a/example/test_files/make_public.f90 b/example/test_files/make_public.f90 new file mode 100644 index 00000000..85cac3ef --- /dev/null +++ b/example/test_files/make_public.f90 @@ -0,0 +1,36 @@ +module a_mod + + ! Access_Stmt + private + + ! Attr_Spec with 0 and 1 additional attribute + REAL, protected :: planet_radius = 123 + REAL, parameter, protected :: planet_radius_constant = 123 + + LOGICAL :: public_protected = .FALSE. + LOGICAL :: only_protected = .FALSE. + LOGICAL :: private_protected = .FALSE. + + ! Access_stmt + PUBLIC :: public_protected + ! Protected_Stmt + PROTECTED :: public_protected, only_protected + ! Access_stmt + private :: private_protected + + type :: my_type + ! Private_Components_Stmt + private + integer :: a, b + contains + private + + end type my_type + + ! Access_Spec + type(my_type), private :: my_var + +contains + subroutine sub_a + end subroutine sub_a +end module a_mod \ No newline at end of file diff --git a/example/test_files/make_public_correct.f90 b/example/test_files/make_public_correct.f90 new file mode 100644 index 00000000..af58de56 --- /dev/null +++ b/example/test_files/make_public_correct.f90 @@ -0,0 +1,16 @@ +MODULE a_mod + REAL :: planet_radius = 123 + REAL, PARAMETER :: planet_radius_constant = 123 + LOGICAL :: public_protected = .FALSE. + LOGICAL :: only_protected = .FALSE. + LOGICAL :: private_protected = .FALSE. + PUBLIC :: public_protected + TYPE :: my_type + INTEGER :: a, b + CONTAINS + END TYPE my_type + TYPE(my_type), PUBLIC :: my_var + CONTAINS + SUBROUTINE sub_a + END SUBROUTINE sub_a +END MODULE a_mod diff --git a/example/test_files/split_file/test.f90 b/example/test_files/split_file/test.f90 index 56ac3dbc..f550a2a0 100644 --- a/example/test_files/split_file/test.f90 +++ b/example/test_files/split_file/test.f90 @@ -1,22 +1,21 @@ -module mod1 - integer :: a -contains - subroutine p(args) - implicit none - real :: args - - end subroutine p -end module mod1 +MODULE mod1 + INTEGER :: a + CONTAINS + SUBROUTINE p(args) + IMPLICIT NONE + REAL :: args -subroutine sub() -end subroutine sub + END SUBROUTINE p +END MODULE mod1 -real function func(args) - use mod1 - func = 1 -end function func +SUBROUTINE sub +END SUBROUTINE sub -program test_prog - use mod1 -end program test_prog +REAL FUNCTION func(args) + USE mod1 + func = 1 +END FUNCTION func +PROGRAM test_prog + USE mod1 +END PROGRAM test_prog diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py index 8cc4baec..afc30452 100644 --- a/src/fparser/common/tests/test_readfortran.py +++ b/src/fparser/common/tests/test_readfortran.py @@ -44,7 +44,6 @@ import io import os.path -import tempfile import pytest from fparser.common.readfortran import ( @@ -787,12 +786,11 @@ def test_multi_put_item(ignore_comments): ############################################################################## -def test_filename_reader(): +def test_filename_reader(tmpdir): """ Tests that a Fortran source file can be read given its filename. """ - handle, filename = tempfile.mkstemp(suffix=".f90", text=True) - os.close(handle) + filename = f"{tmpdir}/out.f90" try: with io.open(filename, mode="w", encoding="UTF-8") as source_file: source_file.write(FULL_FREE_SOURCE) @@ -811,12 +809,11 @@ def test_filename_reader(): ############################################################################## -def test_file_reader(): +def test_file_reader(tmpdir): """ Tests that a Fortran source file can be read given a file object of it. """ - handle, filename = tempfile.mkstemp(suffix=".f90", text=True) - os.close(handle) + filename = f"{tmpdir}/out.f90" try: with io.open(filename, mode="w", encoding="UTF-8") as source_file: source_file.write(FULL_FREE_SOURCE) @@ -833,12 +830,11 @@ def test_file_reader(): raise -def test_none_in_fifo(log): +def test_none_in_fifo(tmpdir, log): """Check that a None entry in the reader FIFO buffer is handled correctly.""" log.reset() - handle, filename = tempfile.mkstemp(suffix=".f90", text=True) - os.close(handle) + filename = f"{tmpdir}/out.f90" with io.open(filename, mode="w", encoding="UTF-8") as source_file: source_file.write(FULL_FREE_SOURCE) @@ -913,7 +909,7 @@ def test_reader_ignore_encoding(reader_cls, tmp_path): assert reader2.format == FortranFormat(False, True) -def test_inherited_f77(): +def test_inherited_f77(tmpdir): """ A grab bag of functional tests inherited from readfortran.py. """ @@ -948,8 +944,8 @@ def test_inherited_f77(): assert str(item) == stack.pop(0) # Reading from file - handle, filename = tempfile.mkstemp(suffix=".f", text=True) - os.close(handle) + filename = f"{tmpdir}/out.f" + with open(filename, "w") as fortran_file: print(string_f77, file=fortran_file) diff --git a/src/fparser/common/tests/test_sourceinfo.py b/src/fparser/common/tests/test_sourceinfo.py index 5df185ad..5f4aa57b 100644 --- a/src/fparser/common/tests/test_sourceinfo.py +++ b/src/fparser/common/tests/test_sourceinfo.py @@ -35,6 +35,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ############################################################################## # Modified M.Hambley, UK Met Office +# Modified by J. Henrichs, Bureau of Meteorology ############################################################################## """ Test battery associated with fparser.sourceinfo package. @@ -315,7 +316,7 @@ def extension(request): ############################################################################## -def test_get_source_info_filename(extension, header, content): +def test_get_source_info_filename(tmpdir, extension, header, content): # pylint: disable=redefined-outer-name """ Tests that source format is correctly identified when read from a file. @@ -326,8 +327,7 @@ def test_get_source_info_filename(extension, header, content): if content[0] is not None: full_source += content[0] - source_file, filename = tempfile.mkstemp(suffix=extension[0], text=True) - os.close(source_file) + filename = f"{tmpdir}/out{extension[0]}" with open(filename, "w") as source_file: print(full_source, file=source_file) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 4ce1fb61..3ce0ad3c 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -252,19 +252,23 @@ class Program(BlockBase): # R201 use_names = ["Program_Unit"] @show_result - def __new__(cls, string): + def __new__(cls, string, _deepcopy=False): """Wrapper around base class __new__ to catch an internal NoMatchError exception and raise it as an external FortranSyntaxError exception. :param type cls: the class of object to create :param string: (source of) Fortran string to parse :type string: :py:class:`FortranReaderBase` + :param _deepcopy: Flag to signal whether this class is + created by a deep copy + :type _deepcopy: bool + :raises FortranSyntaxError: if the code is not valid Fortran """ # pylint: disable=unused-argument try: - return Base.__new__(cls, string) + return Base.__new__(cls, string, _deepcopy=_deepcopy) except NoMatchError: # At the moment there is no useful information provided by # NoMatchError so we pass on an empty string. @@ -277,6 +281,18 @@ def __new__(cls, string): # provides line number information). raise FortranSyntaxError(string, excinfo) + def __getnewargs__(self): + """Method to dictate the values passed to the __new__() method upon + unpickling. The method must return a pair (args, kwargs) where + args is a tuple of positional arguments and kwargs a dictionary + of named arguments for constructing the object. Those will be + passed to the __new__() method upon unpickling. + + :return: set of arguments for __new__ + :rtype: tuple[str, bool] + """ + return (self.string, True) + @staticmethod def match(reader): """Implements the matching for a Program. Whilst the rule looks like @@ -8506,14 +8522,19 @@ class Stop_Code(StringBase): # R850 = | [ [ [ [ ] ] ] ] - + Extension: + | Level_3_Expr """ subclass_names = ["Scalar_Char_Constant"] @staticmethod def match(string): - return StringBase.match(pattern.abs_label, string) + result = StringBase.match(pattern.abs_label, string) + if result or not "extended-stop-args" in EXTENSIONS(): + return result + # This will allow statements like `stop -1` and `stop str1//str2` + return Level_3_Expr(string) # diff --git a/src/fparser/two/Fortran2008/__init__.py b/src/fparser/two/Fortran2008/__init__.py index 3a04c8a4..fe135021 100644 --- a/src/fparser/two/Fortran2008/__init__.py +++ b/src/fparser/two/Fortran2008/__init__.py @@ -1,7 +1,7 @@ # ----------------------------------------------------------------------------- # BSD 3-Clause License # -# Copyright (c) 2023, Science and Technology Facilities Council. +# Copyright (c) 2023-2024, Science and Technology Facilities Council. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -66,6 +66,7 @@ from fparser.two.Fortran2008.loop_control_r818 import Loop_Control from fparser.two.Fortran2008.if_stmt_r837 import If_Stmt from fparser.two.Fortran2008.error_stop_stmt_r856 import Error_Stop_Stmt +from fparser.two.Fortran2008.stop_code_r857 import Stop_Code from fparser.two.Fortran2008.specification_part_c1112 import Specification_Part_C1112 from fparser.two.Fortran2008.implicit_part_c1112 import Implicit_Part_C1112 from fparser.two.Fortran2008.implicit_part_stmt_c1112 import Implicit_Part_Stmt_C1112 diff --git a/src/fparser/two/Fortran2008/stop_code_r857.py b/src/fparser/two/Fortran2008/stop_code_r857.py new file mode 100644 index 00000000..5628f861 --- /dev/null +++ b/src/fparser/two/Fortran2008/stop_code_r857.py @@ -0,0 +1,50 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 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. +# ----------------------------------------------------------------------------- + +""" + Module containing Fortran2008 Error_Stop_Stmt rule R857 +""" +from fparser.two.utils import Base + + +class Stop_Code(Base): # R857 + """ + Fortran2008 rule R867. Changes the allowed stop code type. + + stop-code is scalar-default-char-constant-expr + or scalar-int-constant-expr + """ + + subclass_names = ["Scalar_Default_Char_Expr", "Scalar_Int_Expr"] + use_names = ["Stop_Code"] diff --git a/src/fparser/two/tests/fortran2008/test_stop_code_r857.py b/src/fparser/two/tests/fortran2008/test_stop_code_r857.py new file mode 100644 index 00000000..12424cfd --- /dev/null +++ b/src/fparser/two/tests/fortran2008/test_stop_code_r857.py @@ -0,0 +1,96 @@ +# Copyright (c) 2024 Science and Technology Facilities Council + +# All rights reserved. + +# Modifications made as part of the fparser project are distributed +# under the following license: + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: + +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# 2. 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. + +# 3. 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. + +"""Test Fortran 2008 rule R856 + + error-stop-stmt is ERROR STOP [ stop-code ] +""" + +import pytest + +from fparser.api import get_reader +from fparser.two.utils import NoMatchError, walk +from fparser.two import Fortran2003, utils +from fparser.two.Fortran2008 import Stop_Code + + +@pytest.mark.usefixtures("f2008_create") +@pytest.mark.parametrize("string", ["1", "- 1", '"abc"', "'abc'", "'abc' // 'def'"]) +def test_simple_stop_code(string): + """Test that error-stop matches the expected valid values.""" + result = Stop_Code(string) + assert str(result) == string + + +@pytest.mark.usefixtures("f2008_create") +@pytest.mark.parametrize("string", ["call sub()", "do i", "1, 2, 3"]) +def test_simple_stop_code_errors(string): + """Test that invalid stop codes are handled.""" + with pytest.raises(NoMatchError) as err: + Stop_Code(string) + assert f"Stop_Code: '{string}'" in str(err.value) + + +@pytest.mark.parametrize("string", ["1", "12345"]) +def test_stop_stmt_2003_stop_code(f2008_parser, string): + """Test that 'stop' parsing works in real code, and returns a 2003 + StopCode. This is the case if the stop code is between one and + five digits only: + """ + code = f""" + subroutine dummy() + stop {string} + end subroutine dummy + """ + tree = f2008_parser(get_reader(code)) + stop_code = walk(tree, Fortran2003.Stop_Code)[0] + assert str(stop_code) == string + + +@pytest.mark.parametrize("string", ["1234567", "12 .AND. 34"]) +def test_stop_stmt_2008(f2008_parser, string, monkeypatch): + """Test that stop parsing works in real code when using F2008 + only (i.e. not F2003) statements. Note that '12 .and. 34' is a + level-5-expr, and as such would not be accepted by the F2003 + "extended-stop-args" extension in fparser. + """ + monkeypatch.setattr(utils, "_EXTENSIONS", []) + code = f""" + subroutine dummy() + stop {string} + end subroutine dummy + """ + tree = f2008_parser(get_reader(code)) + stop_stmt = walk(tree, Fortran2003.Stop_Stmt)[0] + assert str(stop_stmt.children[1]) == string diff --git a/src/fparser/two/tests/test_fortran2003.py b/src/fparser/two/tests/test_fortran2003.py index 1993ae9d..e1cc78fc 100644 --- a/src/fparser/two/tests/test_fortran2003.py +++ b/src/fparser/two/tests/test_fortran2003.py @@ -1,4 +1,4 @@ -# Modified work Copyright (c) 2017-2023 Science and Technology +# Modified work Copyright (c) 2017-2024 Science and Technology # Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson # @@ -68,10 +68,12 @@ """ import pytest + from fparser.two.Fortran2003 import * from fparser.two import Fortran2003 from fparser.two.symbol_table import SYMBOL_TABLES from fparser.two.utils import NoMatchError +from fparser.two import utils from fparser.api import get_reader @@ -2096,7 +2098,17 @@ def test_continue_stmt(): # R848 assert repr(obj) == "Continue_Stmt('CONTINUE')" -def test_stop_stmt(): # R849 +@pytest.mark.parametrize("standard_only", [True, False]) +def test_stop_stmt_standard_2003(standard_only, monkeypatch): + """Test that stop statements are parsed correctly [R849]. + It tests both pure 2003 standard compliance, but also + that negative numbers and string concatenations are accepted. + """ + if standard_only: + # Disable the stop-stmt extension for this test to verify + # that really only standard expressions are accepted + monkeypatch.setattr(utils, "_EXTENSIONS", []) + tcls = Stop_Stmt obj = tcls("stop") assert isinstance(obj, tcls), repr(obj) @@ -2110,6 +2122,33 @@ def test_stop_stmt(): # R849 assert isinstance(obj, tcls), repr(obj) assert str(obj) == "STOP 'hey you'" + # This should not be accepted even with the extension enabled: + with pytest.raises(NoMatchError) as excinfo: + tcls("stop 12 .and. 34") + assert "Stop_Stmt: 'stop 12 .and. 34'" in str(excinfo.value) + + if standard_only: + # This should not be accepted according to F2003 + with pytest.raises(NoMatchError) as excinfo: + tcls('stop "123"//"456"') + assert 'Stop_Stmt: \'stop "123"//"456"' in str(excinfo.value) + + # This should not be accepted according to F2003 + with pytest.raises(NoMatchError) as excinfo: + tcls("stop -321") + assert "Stop_Stmt: 'stop -321'" in str(excinfo.value) + + else: + # Test the F2003 standard extensions, which should + # accept these expressions + obj = tcls('stop "123"//"456"') + assert isinstance(obj, tcls), repr(obj) + assert str(obj) == 'STOP "123" // "456"' + + obj = tcls("stop -321") + assert isinstance(obj, tcls), repr(obj) + assert str(obj) == "STOP - 321" + # # SECTION 9 diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 5d4a6e19..9552eaa9 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -36,7 +36,7 @@ import pytest from fparser.two.parser import ParserFactory from fparser.common.readfortran import FortranStringReader -from fparser.two.utils import FortranSyntaxError +from fparser.two.utils import FortranSyntaxError, StmtBase from fparser.two.symbol_table import SYMBOL_TABLES from fparser.two import Fortran2003, Fortran2008 @@ -84,3 +84,112 @@ class do not affect current calls. with pytest.raises(ValueError) as excinfo: parser = ParserFactory().create(std="invalid") assert "is an invalid standard" in str(excinfo.value) + + +def _cmp_tree_types_rec( + node1: Fortran2003.Program, node2: Fortran2003.Program, depth: int = 0 +): + """Helper function to recursively check for deepcopied programs + + :param node1: First AST tree to check + :type node1: Fortran2003.Program + :param node2: Second AST tree to check + :type node2: Fortran2003.Program + :param depth: Depth useful later on for debugging reasons, + defaults to 0 + :type depth: int, optional + """ + + # Make sure that both trees are the same + assert type(node1) is type( + node2 + ), f"Nodes have different types: '{type(node1)}' and '{type(node2)}" + + if node1 is None: + # Just return for None objects + return + + if type(node1) is str: + # WARNING: Different string objects with the same can have the same id. + # Therefore, we can't compare with 'is' or with 'id(.) == id(.)'. + # We can just compare the both strings have the same content. + # See https://stackoverflow.com/questions/20753364/why-does-creating-multiple-objects-without-naming-them-result-in-them-having-the + assert node1 == node2 + return + + else: + # Make sure that we're working on a copy rather than the same object + assert node1 is not node2, "Nodes refer to the same object" + + # Continue recursive traversal of ast + for child1, child2 in zip(node1.children, node2.children): + _cmp_tree_types_rec(child1, child2, depth + 1) + + +_f90_source_test = """ +module andy +implicit none + + real :: apple = 1.0 + real, parameter :: pi = 3.14 + +contains + subroutine sergi() + print *, "Pi = ", pi + print *, "apple = ", apple + end subroutine + +end module andy + + +program awesome + use andy + implicit none + + real :: x + integer :: i + + x = 2.2 + i = 7 + + call sergi() + + print *, "apple pie: ", apple, pi + print *, "i: ", i + +end program awesome + +""" + + +def test_deepcopy(): + """ + Test that we can deepcopy a parsed fparser tree. + """ + + parser = ParserFactory().create(std="f2008") + reader = FortranStringReader(_f90_source_test) + ast = parser(reader) + + import copy + + new_ast = copy.deepcopy(ast) + + _cmp_tree_types_rec(new_ast, ast) + + +def test_pickle(): + """ + Test that we can pickle and unpickle a parsed fparser tree. + """ + + parser = ParserFactory().create(std="f2008") + reader = FortranStringReader(_f90_source_test) + ast = parser(reader) + + import pickle + + s = pickle.dumps(ast) + new_ast = pickle.loads(s) + + _cmp_tree_types_rec(new_ast, ast) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 04c65005..de2731df 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -114,6 +114,11 @@ # when reading/writing data using unformatted IO. _EXTENSIONS += ["open-convert"] +# While non-standard, many compilers support negative numbers, and string +# operations in stop statements, e.g. `stop -1` or `stop str1//str2`. +# With this extension, these statements will be allowed. +_EXTENSIONS += ["extended-stop-args"] + def EXTENSIONS(): """ @@ -404,7 +409,7 @@ def __init__(self, string, parent_cls=None): self.parent = None @show_result - def __new__(cls, string, parent_cls=None): + def __new__(cls, string, parent_cls=None, _deepcopy=False): if parent_cls is None: parent_cls = [cls] elif cls not in parent_cls: @@ -413,6 +418,11 @@ def __new__(cls, string, parent_cls=None): # Get the class' match method if it has one match = getattr(cls, "match", None) + if _deepcopy: + # If this is part of a deep-copy operation (and string is None), simply call + # the super method without string + return super().__new__(cls) + if ( isinstance(string, FortranReaderBase) and match @@ -500,6 +510,18 @@ def __new__(cls, string, parent_cls=None): errmsg = f"{cls.__name__}: '{string}'" raise NoMatchError(errmsg) + def __getnewargs__(self): + """Method to dictate the values passed to the __new__() method upon + unpickling. The method must return a pair (args, kwargs) where + args is a tuple of positional arguments and kwargs a dictionary + of named arguments for constructing the object. Those will be + passed to the __new__() method upon unpickling. + + :return: set of arguments for __new__ + :rtype: tuple[str, NoneType, bool] + """ + return (self.string, None, True) + def get_root(self): """ Gets the node at the root of the parse tree to which this node belongs.