Repository: stfc/PSyclone

Issue:
Support a fortran program in the PSyIR
At the moment a Fortran program will produce a code block in the PSyIR when processed using the `fparser2reader` class' `generate_psyir` method. There is an unused option in a `Routine` node (`is_program`) which specifies that the instance is a program. This could be used to translate from an fparser2 ast to PSyIR. Further, the Fortran back-end does not seem to support a Routine's `is_program` option so would also need to be updated.

Discussion:

Patch:
diff --git a/changelog b/changelog
index 6a2c66eec..2ef739173 100644
--- a/changelog
+++ b/changelog
@@ -393,6 +393,8 @@
 	126) PR #1095 for #1083. Adds a LoopTrans base class that Loop
 	transformations can subclass and use any common functionality.
 
+	127) PR #1138 for #1137. Add support for Fortran Program in the PSyIR.
+
 release 1.9.0 20th May 2020
 
 	1) #602 for #597. Modify Node.ancestor() to optionally include
diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py
index 4b6c12b2a..e03cdcc1f 100644
--- a/src/psyclone/psyir/backend/fortran.py
+++ b/src/psyclone/psyir/backend/fortran.py
@@ -732,10 +732,13 @@ class FortranWriter(PSyIRVisitor):
         if not node.name:
             raise VisitorError("Expected node name to have a value.")
 
-        args = [symbol.name for symbol in node.symbol_table.argument_list]
-        result = (
-            "{0}subroutine {1}({2})\n"
-            "".format(self._nindent, node.name, ", ".join(args)))
+        if node.is_program:
+            result = ("{0}program {1}\n".format(self._nindent, node.name))
+        else:
+            args = [symbol.name for symbol in node.symbol_table.argument_list]
+            result = (
+                "{0}subroutine {1}({2})\n"
+                "".format(self._nindent, node.name, ", ".join(args)))
 
         self._depth += 1
 
@@ -773,9 +776,13 @@ class FortranWriter(PSyIRVisitor):
             "".format(imports, declarations, exec_statements))
 
         self._depth -= 1
+        if node.is_program:
+            keyword = "program"
+        else:
+            keyword = "subroutine"
         result += (
-            "{0}end subroutine {1}\n"
-            "".format(self._nindent, node.name))
+            "{0}end {1} {2}\n"
+            "".format(self._nindent, keyword, node.name))
 
         return result
 
diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py
index a2b167a21..6a182367e 100644
--- a/src/psyclone/psyir/frontend/fparser2.py
+++ b/src/psyclone/psyir/frontend/fparser2.py
@@ -768,6 +768,7 @@ class Fparser2Reader(object):
             Fortran2003.Call_Stmt: self._call_handler,
             Fortran2003.Subroutine_Subprogram: self._subroutine_handler,
             Fortran2003.Module: self._module_handler,
+            Fortran2003.Main_Program: self._main_program_handler,
             Fortran2003.Program: self._program_handler,
         }
 
@@ -3436,6 +3437,45 @@ class Fparser2Reader(object):
 
         return routine
 
+    def _main_program_handler(self, node, parent):
+        '''Transforms an fparser2 Main_Program statement into a PSyIR
+        Routine node.
+
+        :param node: node in fparser2 parse tree.
+        :type node: :py:class:`fparser.two.Fortran2003.Main_Program`
+        :param parent: parent node of the PSyIR node being constructed.
+        :type parent: subclass of :py:class:`psyclone.psyir.nodes.Node`
+
+        :returns: PSyIR representation of node.
+        :rtype: :py:class:`psyclone.psyir.nodes.Routine`
+
+        '''
+        name = node.children[0].children[1].string
+        routine = Routine(name, parent=parent, is_program=True)
+
+        try:
+            prog_spec = _first_type_match(node.content,
+                                          Fortran2003.Specification_Part)
+            decl_list = prog_spec.content
+        except ValueError:
+            # program has no Specification_Part so has no
+            # declarations. Continue with empty list.
+            decl_list = []
+        finally:
+            self.process_declarations(routine, decl_list, [])
+
+        try:
+            prog_exec = _first_type_match(node.content,
+                                          Fortran2003.Execution_Part)
+        except ValueError:
+            # Routines without any execution statements are still
+            # valid.
+            pass
+        else:
+            self.process_nodes(routine, prog_exec.content)
+
+        return routine
+
     def _module_handler(self, node, parent):
         '''Transforms an fparser2 Module statement into a PSyIR Container node.
 


Test patch:
diff --git a/src/psyclone/tests/psyir/backend/fortran_test.py b/src/psyclone/tests/psyir/backend/fortran_test.py
index 138950100..be4738927 100644
--- a/src/psyclone/tests/psyir/backend/fortran_test.py
+++ b/src/psyclone/tests/psyir/backend/fortran_test.py
@@ -945,6 +945,34 @@ def test_fw_routine(fort_writer, monkeypatch, tmpdir):
         _ = fort_writer(schedule)
     assert "Expected node name to have a value." in str(excinfo.value)
 
+
+def test_fw_routine_program(parser, fort_writer, tmpdir):
+    '''Check the FortranWriter class outputs correct code when a routine node
+    is found with is_program set to True i.e. it should be output as a program.
+
+    '''
+    # Generate PSyIR from Fortran code via fparser2 ast
+    code = (
+        "program test\n"
+        "  real :: a\n"
+        "  a = 0.0\n"
+        "end program test")
+    reader = FortranStringReader(code)
+    fp2_ast = parser(reader)
+    fp2_reader = Fparser2Reader()
+    psyir = fp2_reader.generate_psyir(fp2_ast)
+
+    # Generate Fortran from PSyIR
+    result = fort_writer(psyir)
+
+    assert(
+        "program test\n"
+        "  real :: a\n\n"
+        "  a = 0.0\n\n"
+        "end program test\n" in result)
+    assert Compile(tmpdir).string_compiles(result)
+
+
 # assignment and binaryoperation (not intrinsics) are already checked
 # within previous tests
 
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_generate_psyir_test.py b/src/psyclone/tests/psyir/frontend/fparser2_generate_psyir_test.py
index 9fd84b4d2..549f9fcaf 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_generate_psyir_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_generate_psyir_test.py
@@ -83,10 +83,10 @@ PROGRAM_IN = (
     "a=0.0\n"
     "end program main\n")
 PROGRAM_OUT = (
-    "PROGRAM main\n"
-    "  REAL :: a\n"
-    "  a = 0.0\n"
-    "END PROGRAM main")
+    "program main\n"
+    "  real :: a\n\n"
+    "  a = 0.0\n\n"
+    "end program main\n")
 FUNCTION_IN = (
     "integer function tmp(a)\n"
     "real :: a\n"
@@ -102,7 +102,7 @@ FUNCTION_OUT = (
 @pytest.mark.parametrize("code,expected,node_class",
                          [(MODULE_IN, MODULE_OUT, Container),
                           (SUB_IN, SUB_OUT, Routine),
-                          (PROGRAM_IN, PROGRAM_OUT, CodeBlock),
+                          (PROGRAM_IN, PROGRAM_OUT, Routine),
                           (FUNCTION_IN, FUNCTION_OUT, CodeBlock)])
 def test_generate_psyir(parser, code, expected, node_class):
     '''Test that generate_psyir generates PSyIR from an fparser2 parse
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_main_program_handler_test.py b/src/psyclone/tests/psyir/frontend/fparser2_main_program_handler_test.py
new file mode 100644
index 000000000..7362de4d5
--- /dev/null
+++ b/src/psyclone/tests/psyir/frontend/fparser2_main_program_handler_test.py
@@ -0,0 +1,96 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021, 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 R. W. Ford, STFC Daresbury Lab
+
+'''Module containing pytest tests for the _main_program_handler method
+in the class Fparser2Reader. This handler deals with the translation
+of the fparser2 Main_Program construct to PSyIR.
+
+'''
+from __future__ import absolute_import
+import pytest
+
+from fparser.common.readfortran import FortranStringReader
+from psyclone.psyir.nodes import Routine
+from psyclone.psyir.frontend.fparser2 import Fparser2Reader
+from psyclone.psyir.backend.fortran import FortranWriter
+
+# program no declarations
+PROG1_IN = (
+    "program prog\n"
+    "end program prog\n")
+PROG1_OUT = (
+    "program prog\n\n\n"
+    "end program prog\n")
+# program with symbols/declarations
+PROG2_IN = (
+    "program prog\n"
+    "real :: a\n"
+    "end program\n")
+PROG2_OUT = (
+    "program prog\n"
+    "  real :: a\n\n\n"
+    "end program prog\n")
+# program with executable content
+PROG3_IN = (
+    "program prog\n"
+    "real :: a\n"
+    "a=0.0\n"
+    "end\n")
+PROG3_OUT = (
+    "program prog\n"
+    "  real :: a\n\n"
+    "  a = 0.0\n\n"
+    "end program prog\n")
+
+
+@pytest.mark.parametrize("code,expected",
+                         [(PROG1_IN, PROG1_OUT),
+                          (PROG2_IN, PROG2_OUT),
+                          (PROG3_IN, PROG3_OUT)])
+def test_main_program_handler(parser, code, expected):
+    '''Test that main_program_handler handles valid Fortran subroutines.'''
+
+    processor = Fparser2Reader()
+    reader = FortranStringReader(code)
+    parse_tree = parser(reader)
+    program = parse_tree.children[0]
+    psyir = processor._main_program_handler(program, None)
+    # Check the expected PSyIR nodes are being created
+    assert isinstance(psyir, Routine)
+    assert psyir.is_program
+    assert psyir.parent is None
+    writer = FortranWriter()
+    result = writer(psyir)
+    assert expected == result