Repository: stfc/fparser

Issue:
Syntax error when parsing source which INCLUDEs an INTERFACE block.
Parsing the following code:

          PROGRAM test_include
          INCLUDE 'mpif.h'
          WRITE(*,*) MPI_COMM_WORLD
          END

with the include path set to find the NVIDIA mpi header files gives:

    File: 'fixed_form.f'
    Syntax error: at line 2
    >>>      INCLUDE 'mpif.h'

irrespective of whether the reader is set to free or fixed format.


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b7f83b..151f365 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,8 +18,11 @@ Modifications by (in alphabetical order):
 * P. Vitt, University of Siegen, Germany
 * A. Voysey, UK Met Office
 
-15/05/2023 PR #415 for #165. Bug fix for spurious matching of 'NAMELIST' in
-           certain contexts.
+16/05/2023 PR #414 for #412. Bug fix for disappearing line when parsing
+           include files.
+
+15/05/2023 PR #415 for #165. Bug fix for code aborting when trying to match
+           'NAMELIST' in certain contexts.
 
 15/05/2023 PR #408 for #403. Add support for the F2008 DO CONCURRENT.
 
diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py
index 16480fd..a5f886d 100644
--- a/src/fparser/common/readfortran.py
+++ b/src/fparser/common/readfortran.py
@@ -745,8 +745,19 @@ class FortranReaderBase:
         return item
 
     def put_item(self, item):
-        """Insert item to FIFO item buffer."""
-        self.fifo_item.insert(0, item)
+        """Insert item into FIFO buffer of 'innermost' reader object.
+
+        :param item: the item to insert into the FIFO.
+        :type item: :py:class:`fparser.common.readfortran.Line` | \
+                    :py:class:`fparser.common.readfortran.MultiLine` | \
+                    :py:class:`fparser.common.readfortran.Comment`
+        """
+        if self.reader:
+            # We are reading an INCLUDE file so put this item in the FIFO
+            # of the corresponding reader.
+            self.reader.put_item(item)
+        else:
+            self.fifo_item.insert(0, item)
 
     # Iterator methods:
 
@@ -767,7 +778,7 @@ class FortranReaderBase:
         value.
 
         :returns: the next line item. This can be from a local fifo \
-        buffer, from an include reader or from this reader.
+                  buffer, from an include reader or from this reader.
         :rtype: py:class:`fparser.common.readfortran.Line`
 
         :raises StopIteration: if no more lines are found.
@@ -780,7 +791,6 @@ class FortranReaderBase:
             if self.reader is not None:
                 # inside INCLUDE statement
                 try:
-                    # Return a line from the include.
                     return self.reader.next(ignore_comments)
                 except StopIteration:
                     # There is nothing left in the include


Test patch:
diff --git a/src/fparser/common/tests/test_readfortran.py b/src/fparser/common/tests/test_readfortran.py
index a3b7f5a..05d48bb 100644
--- a/src/fparser/common/tests/test_readfortran.py
+++ b/src/fparser/common/tests/test_readfortran.py
@@ -680,20 +680,46 @@ def test_put_item(ignore_comments):
         assert fifo_line == orig_line
 
 
-def test_put_item_include(ignore_comments):
+def test_put_item_include(ignore_comments, tmpdir):
     """Check that when a line that has been included via an include
     statement is consumed it can be pushed back so it can be consumed
     again. Test with and without ignoring comments.
 
     """
-    reader = FortranStringReader(FORTRAN_CODE, ignore_comments=ignore_comments)
+    _ = tmpdir.chdir()
+    cwd = str(tmpdir)
+    include_code = """
+    var1 = 1
+    var2 = 2
+    var3 = 3
+"""
+    with open(os.path.join(cwd, "my_include.h"), "w") as cfile:
+        cfile.write(include_code)
+    code = """
+program my_prog
+  integer :: var1, var2, var3
+  include 'my_include.h'
+end program my_prog
+"""
+    reader = FortranStringReader(code, ignore_comments=ignore_comments)
+    lines = []
     while True:
-        orig_line = reader.get_item()
-        if not orig_line:
+        lines.append(reader.get_item())
+        # Try immediately putting the line back and then requesting it again.
+        # This checks that the correct FIFO buffer is being used.
+        reader.put_item(lines[-1])
+        assert reader.get_item().line == lines[-1].line
+        if "var3 =" in lines[-1].line:
+            # Stop reading while we're still in the INCLUDE file so that we
+            # have a stack of readers when calling `put_item` below.
             break
-        reader.put_item(orig_line)
-        fifo_line = reader.get_item()
-        assert fifo_line == orig_line
+    # Put all the lines back in the same order that we saw them.
+    for line in reversed(lines):
+        reader.put_item(line)
+    # Check that the reader returns all those lines in the same order that
+    # we saw them originally.
+    for line in lines:
+        assert reader.get_item().line == line.line
 
 
 def test_multi_put_item(ignore_comments):