Repository: stfc/PSyclone

Issue:
Add support for Functions to fparser2 frontend
Currently, Fortran functions are put into CodeBlocks by the fparser2 frontend. However, we need them represented as `Routine` nodes in order to process NEMO source.

Discussion:

Patch:
diff --git a/changelog b/changelog
index 1fcf337cf..0a32cfa05 100644
--- a/changelog
+++ b/changelog
@@ -24,6 +24,9 @@
         9) PR #1225 towards #1223. Moves remaining GOcean constants into
         new GOceanConstants class.
 
+        10) PR #1241 for #1240. Adds support for functions in the PSyIR and
+		Fortran frontend and backend.
+
 release 2.0.0 28th April 2021
 
 	1) #778 for #713. Use 'make' to execute all examples.
diff --git a/doc/developer_guide/psyir.rst b/doc/developer_guide/psyir.rst
index 310606f67..85d59e5c9 100644
--- a/doc/developer_guide/psyir.rst
+++ b/doc/developer_guide/psyir.rst
@@ -286,9 +286,10 @@ Routine
 
 The `Routine` node is a subclass of `Schedule` that represents any program
 unit (subroutine, function or main program). As such it extends `Schedule`
-through the addition of the `return_type` and `is_program` properties.
+through the addition of the `return_symbol` (required when representing a
+function) and `is_program` properties.
 It also adds the `create` helper method for constructing a valid
-`Routine` instance.It is an important node in PSyclone because two of its
+`Routine` instance. It is an important node in PSyclone because two of its
 specialisations: `InvokeSchedule` and `KernelSchedule` (described below),
 are used as the root nodes of PSy-layer invokes and kernel subroutines.
 This makes them the starting points for any walking of the PSyIR tree in
diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py
index 43dd02f98..33ea9993a 100644
--- a/src/psyclone/psyir/backend/fortran.py
+++ b/src/psyclone/psyir/backend/fortran.py
@@ -741,14 +741,14 @@ class FortranWriter(PSyIRVisitor):
         '''This method is called when a Routine node is found in
         the PSyIR tree.
 
-        :param node: a KernelSchedule PSyIR node.
-        :type node: :py:class:`psyclone.psyir.nodes.KernelSchedule`
+        :param node: a Routine PSyIR node.
+        :type node: :py:class:`psyclone.psyir.nodes.Routine`
 
-        :returns: the Fortran code as a string.
+        :returns: the Fortran code for this node.
         :rtype: str
 
         :raises VisitorError: if the name attribute of the supplied \
-        node is empty or None.
+                              node is empty or None.
 
         '''
         if not node.name:
@@ -756,11 +756,20 @@ class FortranWriter(PSyIRVisitor):
 
         if node.is_program:
             result = ("{0}program {1}\n".format(self._nindent, node.name))
+            routine_type = "program"
         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)))
+            suffix = ""
+            if node.return_symbol:
+                # This Routine has a return value and is therefore a Function
+                routine_type = "function"
+                if node.return_symbol.name != node.name:
+                    suffix = " result({0})".format(node.return_symbol.name)
+            else:
+                routine_type = "subroutine"
+            result = "{0}{1} {2}({3}){4}\n".format(self._nindent, routine_type,
+                                                   node.name, ", ".join(args),
+                                                   suffix)
 
         self._depth += 1
 
@@ -798,13 +807,9 @@ class FortranWriter(PSyIRVisitor):
             "".format(imports, declarations, exec_statements))
 
         self._depth -= 1
-        if node.is_program:
-            keyword = "program"
-        else:
-            keyword = "subroutine"
         result += (
             "{0}end {1} {2}\n"
-            "".format(self._nindent, keyword, node.name))
+            "".format(self._nindent, routine_type, node.name))
 
         return result
 
diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py
index f9a8c511b..48289af14 100644
--- a/src/psyclone/psyir/frontend/fparser2.py
+++ b/src/psyclone/psyir/frontend/fparser2.py
@@ -748,6 +748,7 @@ class Fparser2Reader(object):
         self.handlers = {
             Fortran2003.Assignment_Stmt: self._assignment_handler,
             Fortran2003.Data_Ref: self._data_ref_handler,
+            Fortran2003.Function_Subprogram: self._subroutine_handler,
             Fortran2003.Name: self._name_handler,
             Fortran2003.Parenthesis: self._parenthesis_handler,
             Fortran2003.Part_Ref: self._part_ref_handler,
@@ -1428,69 +1429,44 @@ class Fparser2Reader(object):
                 raise NotImplementedError("Found unsupported USE statement: "
                                           "'{0}'".format(str(decl)))
 
-    def _process_decln(self, parent, symbol_table, decl, default_visibility,
-                       visibility_map=None):
+    def _process_type_spec(self, parent, type_spec):
         '''
-        Process the supplied fparser2 parse tree for a declaration. For each
-        entity that is declared, a symbol is added to the supplied symbol
-        table.
+        Processes the fparser2 parse tree of a type specification in order to
+        extract the type and precision that are specified.
 
-        :param parent: PSyIR node in which to insert the symbols found.
-        :type parent: :py:class:`psyclone.psyir.nodes.KernelSchedule`
-        :param symbol_table: the symbol table to which to add new symbols.
-        :type symbol_table: py:class:`psyclone.psyir.symbols.SymbolTable`
-        :param decl: fparser2 parse tree of declaration to process.
-        :type decl: :py:class:`fparser.two.Fortran2003.Type_Declaration_Stmt`
-        :param default_visibility: the default visibility of symbols in the \
-                                   current declaration section.
-        :type default_visibility: \
-            :py:class:`psyclone.symbols.Symbol.Visibility`
-        :param visibility_map: mapping of symbol name to visibility (for \
-            those symbols listed in an accessibility statement).
-        :type visibility_map: dict with str keys and \
-            :py:class:`psyclone.psyir.symbols.Symbol.Visibility` values
+        :param parent: the parent of the current PSyIR node under construction.
+        :type parent: :py:class:`psyclone.psyir.nodes.Node`
+        :param type_spec: the fparser2 parse tree of the type specification.
+        :type type_spec: \
+            :py:class:`fparser.two.Fortran2003.Intrinsic_Type_Spec` or \
+            :py:class:`fparser.two.Fortran2003.Declaration_Type_Spec`
+
+        :returns: the type and precision specified by the type-spec.
+        :rtype: 2-tuple of :py:class:`psyclone.psyir.symbols.ScalarType` or \
+            :py:class:`psyclone.psyir.symbols.TypeSymbol` and \
+            :py:class:`psyclone.psyir.symbols.DataSymbol.Precision` or \
+            :py:class:`psyclone.psyir.symbols.DataSymbol` or int or NoneType
 
         :raises NotImplementedError: if an unsupported intrinsic type is found.
-        :raises NotImplementedError: if the declaration is not of an \
-                                     intrinsic type.
-        :raises NotImplementedError: if the save attribute is encountered on a\
-                                     declaration that is not within a module.
-        :raises NotImplementedError: if an unsupported attribute is found.
-        :raises NotImplementedError: if an unsupported intent attribute is \
-                                     found.
-        :raises NotImplementedError: if an unsupported access-spec attribute \
-                                     is found.
-        :raises NotImplementedError: if the allocatable attribute is found on \
-                                     a non-array declaration.
-        :raises InternalError: if an array with defined extent has the \
-                               allocatable attribute.
-        :raises NotImplementedError: if an initialisation expression is found \
-                                     for a variable declaration.
-        :raises NotImplementedError: if an unsupported initialisation \
-                expression is found for a parameter declaration.
-        :raises NotImplementedError: if a character-length specification is \
-                                     found.
-        :raises SymbolError: if a declaration is found for a symbol that is \
-                already present in the symbol table with a defined interface.
+        :raises SymbolError: if a symbol already exists for the name of a \
+            derived type but is not a TypeSymbol.
+        :raises NotImplementedError: if the supplied type specification is \
+            not for an intrinsic type or a derived type.
 
         '''
-        (type_spec, attr_specs, entities) = decl.items
-
         base_type = None
         precision = None
 
-        # Parse the type_spec
-
         if isinstance(type_spec, Fortran2003.Intrinsic_Type_Spec):
             fort_type = str(type_spec.items[0]).lower()
             try:
                 data_name = TYPE_MAP_FROM_FORTRAN[fort_type]
-            except KeyError:
-                raise NotImplementedError(
+            except KeyError as err:
+                six.raise_from(NotImplementedError(
                     "Could not process {0}. Only 'real', 'double "
                     "precision', 'integer', 'logical' and 'character' "
                     "intrinsic types are supported."
-                    "".format(str(decl.items)))
+                    "".format(str(type_spec))), err)
             if fort_type == "double precision":
                 # Fortran double precision is equivalent to a REAL
                 # intrinsic with precision DOUBLE in the PSyIR.
@@ -1501,6 +1477,7 @@ class Fparser2Reader(object):
             if not precision:
                 precision = default_precision(data_name)
             base_type = ScalarType(data_name, precision)
+
         elif isinstance(type_spec, Fortran2003.Declaration_Type_Spec):
             # This is a variable of derived type
             type_name = str(walk(type_spec, Fortran2003.Type_Name)[0])
@@ -1518,14 +1495,65 @@ class Fparser2Reader(object):
             elif not isinstance(type_symbol, TypeSymbol):
                 raise SymbolError(
                     "Search for a TypeSymbol named '{0}' (required by "
-                    "declaration '{1}') found a '{2}' instead.".format(
-                        type_name, str(decl), type(type_symbol).__name__))
+                    "specification '{1}') found a '{2}' instead.".format(
+                        type_name, str(type_spec), type(type_symbol).__name__))
             base_type = type_symbol
+
         else:
-            # Not a supported declaration. This will result in a symbol of
-            # UnknownFortranType.
+            # Not a supported type specification. This will result in a
+            # CodeBlock or UnknownFortranType, depending on the context.
             raise NotImplementedError()
 
+        return base_type, precision
+
+    def _process_decln(self, parent, symbol_table, decl, default_visibility,
+                       visibility_map=None):
+        '''
+        Process the supplied fparser2 parse tree for a declaration. For each
+        entity that is declared, a symbol is added to the supplied symbol
+        table.
+
+        :param parent: PSyIR node in which to insert the symbols found.
+        :type parent: :py:class:`psyclone.psyir.nodes.KernelSchedule`
+        :param symbol_table: the symbol table to which to add new symbols.
+        :type symbol_table: py:class:`psyclone.psyir.symbols.SymbolTable`
+        :param decl: fparser2 parse tree of declaration to process.
+        :type decl: :py:class:`fparser.two.Fortran2003.Type_Declaration_Stmt`
+        :param default_visibility: the default visibility of symbols in the \
+                                   current declaration section.
+        :type default_visibility: \
+            :py:class:`psyclone.symbols.Symbol.Visibility`
+        :param visibility_map: mapping of symbol name to visibility (for \
+            those symbols listed in an accessibility statement).
+        :type visibility_map: dict with str keys and \
+            :py:class:`psyclone.psyir.symbols.Symbol.Visibility` values
+
+        :raises NotImplementedError: if the save attribute is encountered on \
+            a declaration that is not within a module.
+        :raises NotImplementedError: if an unsupported attribute is found.
+        :raises NotImplementedError: if an unsupported intent attribute is \
+            found.
+        :raises NotImplementedError: if an unsupported access-spec attribute \
+            is found.
+        :raises NotImplementedError: if the allocatable attribute is found on \
+            a non-array declaration.
+        :raises InternalError: if an array with defined extent has the \
+            allocatable attribute.
+        :raises NotImplementedError: if an initialisation expression is found \
+            for a variable declaration.
+        :raises NotImplementedError: if an unsupported initialisation \
+            expression is found for a parameter declaration.
+        :raises NotImplementedError: if a character-length specification is \
+            found.
+        :raises SymbolError: if a declaration is found for a symbol that is \
+            already present in the symbol table with a defined interface.
+
+        '''
+        (type_spec, attr_specs, entities) = decl.items
+
+        # Parse the type_spec
+        base_type, precision = self._process_type_spec(parent, type_spec)
+
         # Parse declaration attributes:
         # 1) If no dimension attribute is provided, it defaults to scalar.
         attribute_shape = []
@@ -1988,7 +2016,8 @@ class Fparser2Reader(object):
         kind=KIND(x).
 
         :param type_spec: the fparser2 parse tree of the type specification.
-        :type type_spec: :py:class:`fparser.two.Fortran2003.Intrinsic_Type_Spec
+        :type type_spec: \
+            :py:class:`fparser.two.Fortran2003.Intrinsic_Type_Spec`
         :param psyir_parent: the parent PSyIR node where the new node \
             will be attached.
         :type psyir_parent: :py:class:`psyclone.psyir.nodes.Node`
@@ -3375,11 +3404,12 @@ class Fparser2Reader(object):
         return call
 
     def _subroutine_handler(self, node, parent):
-        '''Transforms an fparser2 Subroutine_Subprogram statement into a PSyIR
-        Routine node.
+        '''Transforms an fparser2 Subroutine_Subprogram or Function_Subprogram
+        statement into a PSyIR Routine node.
 
         :param node: node in fparser2 parse tree.
-        :type node: :py:class:`fparser.two.Fortran2003.Subroutine_Subprogram`
+        :type node: :py:class:`fparser.two.Fortran2003.Subroutine_Subprogram` \
+            or :py:class:`fparser.two.Fortran2003.Function_Subprogram`
         :param parent: parent node of the PSyIR node being constructed.
         :type parent: subclass of :py:class:`psyclone.psyir.nodes.Node`
 
@@ -3390,6 +3420,7 @@ class Fparser2Reader(object):
         name = node.children[0].children[1].string
         routine = Routine(name, parent=parent)
 
+        # Deal with any arguments
         try:
             sub_spec = _first_type_match(node.content,
                                          Fortran2003.Specification_Part)
@@ -3397,7 +3428,8 @@ class Fparser2Reader(object):
             # TODO this if test can be removed once fparser/#211 is fixed
             # such that routine arguments are always contained in a
             # Dummy_Arg_List, even if there's only one of them.
-            if isinstance(node, Fortran2003.Subroutine_Subprogram) and \
+            if isinstance(node, (Fortran2003.Subroutine_Subprogram,
+                                 Fortran2003.Function_Subprogram)) and \
                isinstance(node.children[0].children[2],
                           Fortran2003.Dummy_Arg_List):
                 arg_list = node.children[0].children[2].children
@@ -3412,6 +3444,69 @@ class Fparser2Reader(object):
         finally:
             self.process_declarations(routine, decl_list, arg_list)
 
+        # If this is a function then work out the return type
+        if isinstance(node, Fortran2003.Function_Subprogram):
+            # Check whether the function-stmt has a prefix specifying the
+            # return type.
+            prefix = node.children[0].children[0]
+            if prefix:
+                # If there is anything else in the prefix (PURE, ELEMENTAL or
+                # RECURSIVE) then we will create a CodeBlock for this function.
+                if len(prefix.children) > 1:
+                    raise NotImplementedError()
+                base_type, _ = self._process_type_spec(parent,
+                                                       prefix.children[0])
+            else:
+                base_type = None
+
+            # Check whether the function-stmt has a suffix containing
+            # 'RETURNS'
+            suffix = node.children[0].children[3]
+            if suffix:
+                # Although the suffix can, in principle, contain a proc-
+                # language-binding-spec (e.g. BIND(C, "some_name")), this is
+                # only valid in an interface block and we are dealing with a
+                # function-subprogram here.
+                return_name = suffix.children[0].string
+            else:
+                # Otherwise, the return value of the function is given by
+                # a symbol of the same name.
+                return_name = name
+
+            # Ensure that we have an explicit declaration for the symbol
+            # returned by the function.
+            if return_name in routine.symbol_table:
+                symbol = routine.symbol_table.lookup(return_name)
+                # If the symbol table still contains a RoutineSymbol
+                # for the function name (rather than a DataSymbol)
+                # then there is no explicit declaration within the
+                # function of the variable used to hold the return
+                # value.
+                if isinstance(symbol, RoutineSymbol):
+                    if not base_type:
+                        # The type of the return value was not specified in the
+                        # function prefix either therefore we have no explicit
+                        # type information for it.
+                        raise NotImplementedError()
+                    # Remove the RoutineSymbol ready to replace it with a
+                    # DataSymbol.
+                    routine.symbol_table.remove(symbol)
+
+            if return_name not in routine.symbol_table:
+                # There is no existing declaration for the symbol returned by
+                # the function (because it is specified by the prefix and
+                # suffix of the function declaration). We add one rather than
+                # attempt to recreate the prefix. We have to set shadowing to
+                # True as there is likely to be a RoutineSymbol for this
+                # function in any enclosing Container.
+                routine.symbol_table.new_symbol(return_name,
+                                                symbol_type=DataSymbol,
+                                                datatype=base_type,
+                                                shadowing=True)
+
+            # Update the Routine object with the return symbol.
+            routine.return_symbol = routine.symbol_table.lookup(return_name)
+
         try:
             sub_exec = _first_type_match(node.content,
                                          Fortran2003.Execution_Part)
diff --git a/src/psyclone/psyir/nodes/kernel_schedule.py b/src/psyclone/psyir/nodes/kernel_schedule.py
index d5969376d..6e5cd9d25 100644
--- a/src/psyclone/psyir/nodes/kernel_schedule.py
+++ b/src/psyclone/psyir/nodes/kernel_schedule.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2020, Science and Technology Facilities Council.
+# Copyright (c) 2020-2021, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -45,42 +45,9 @@ class KernelSchedule(Routine):
     '''
     A KernelSchedule is the parent node of the PSyIR for Kernel source code.
 
-    :param str name: kernel subroutine name.
-    :param parent: parent of the KernelSchedule, defaults to None.
-    :type parent: :py:class:`psyclone.psyir.nodes.Node`
-
     '''
     _text_name = "KernelSchedule"
 
-    def __init__(self, name, parent=None):
-        # A kernel does not return anything and is not a main program
-        super(KernelSchedule, self).__init__(name, is_program=False,
-                                             return_type=None,
-                                             parent=parent)
-
-    @classmethod
-    def create(cls, name, symbol_table, children):
-        '''Create a KernelSchedule instance given a name, a symbol table and a
-        list of child nodes. This is a classmethod so that no instance
-        of KernelSchedule is required in order to call it and the classmethod
-        in the base class can be called straightforwardly.
-
-        :param str name: the name of the KernelSchedule.
-        :param symbol_table: the symbol table associated with this \
-            KernelSchedule.
-        :type symbol_table: :py:class:`psyclone.psyGen.SymbolTable`
-        :param children: a list of PSyIR nodes contained in the \
-            KernelSchedule.
-        :type children: list of :py:class:`psyclone.psyir.nodes.Node`
-
-        :returns: a KernelSchedule instance.
-        :rtype: :py:class:`psyclone.psyGen.KernelInstance`
-
-        '''
-        return super(cls, KernelSchedule).create(name, symbol_table, children,
-                                                 is_program=False,
-                                                 return_type=None)
-
 
 # For automatic documentation generation
 __all__ = ["KernelSchedule"]
diff --git a/src/psyclone/psyir/nodes/routine.py b/src/psyclone/psyir/nodes/routine.py
index 048236f70..b8edd4f34 100644
--- a/src/psyclone/psyir/nodes/routine.py
+++ b/src/psyclone/psyir/nodes/routine.py
@@ -40,7 +40,7 @@
 import six
 
 from psyclone.psyir.nodes.schedule import Schedule
-from psyclone.psyir.symbols import DataType, RoutineSymbol
+from psyclone.psyir.symbols import DataSymbol, RoutineSymbol
 from psyclone.psyir.nodes.node import Node
 from psyclone.psyir.symbols.symboltable import SymbolTable
 
@@ -53,8 +53,6 @@ class Routine(Schedule):
     :param str name: the name of this routine.
     :param bool is_program: whether this Routine represents the entry point \
                             into a program (e.g. Fortran Program or C main()).
-    :param return_type: the return-type of this routine.
-    :type return_type: :py:class:`psyclone.psyir.symbols.DataType` or NoneType
     :param parent: the parent node of this Routine node in the PSyIR.
     :type parent: :py:class:`psyclone.psyir.nodes.Node` or NoneType
 
@@ -65,27 +63,22 @@ class Routine(Schedule):
     _children_valid_format = "[Statement]*"
     _text_name = "Routine"
 
-    def __init__(self, name, is_program=False, return_type=None, parent=None):
+    def __init__(self, name, is_program=False, parent=None):
         super(Routine, self).__init__(parent=parent)
 
         # Name is set-up by the name setter property
         self._name = None
         self.name = name
+        self._return_symbol = None
 
         if not isinstance(is_program, bool):
             raise TypeError("Routine argument 'is_program' must be a bool but "
                             "got '{0}'".format(type(is_program).__name__))
         self._is_program = is_program
 
-        if return_type and not isinstance(return_type, DataType):
-            raise TypeError("Routine argument 'return_type' must be of type "
-                            "DataType but got '{0}'".format(
-                                type(return_type).__name__))
-        self._return_type = return_type
-
     @classmethod
     def create(cls, name, symbol_table, children, is_program=False,
-               return_type=None):
+               return_symbol=None):
         '''Create an instance of the supplied class given a name, a symbol
         table and a list of child nodes. This is implemented as a classmethod
         so that it is able to act as a Factory for subclasses - e.g. it
@@ -98,8 +91,9 @@ class Routine(Schedule):
         :type children: list of :py:class:`psyclone.psyir.nodes.Node`
         :param bool is_program: whether this Routine represents the entry \
             point into a program (i.e. Fortran Program or C main()).
-        :param return_type: the return-type of this routine.
-        :type return_type: :py:class:`psyclone.psyir.symbols.DataType` or \
+        :param return_symbol: the Symbol that holds the return value of this \
+            routine (if any). Must be present in the supplied symbol_table.
+        :type return_symbol: :py:class:`psyclone.psyir.symbols.DataType` or \
             NoneType
 
         :returns: an instance of `cls`.
@@ -134,10 +128,11 @@ class Routine(Schedule):
         kern = cls(name)
         # pylint: disable=protected-access
         kern._is_program = is_program
-        kern._return_type = return_type
         kern._symbol_table = symbol_table
         symbol_table._node = kern
         kern.children = children
+        if return_symbol:
+            kern.return_symbol = return_symbol
         return kern
 
     def node_str(self, colour=True):
@@ -208,12 +203,36 @@ class Routine(Schedule):
         return self._is_program
 
     @property
-    def return_type(self):
+    def return_symbol(self):
+        '''
+        :returns: the symbol which will hold the return value of this Routine \
+                  or None if the Routine is not a function.
+        :rtype: :py:class:`psyclone.psyir.symbols.DataSymbol` or NoneType
+        '''
+        return self._return_symbol
+
+    @return_symbol.setter
+    def return_symbol(self, value):
         '''
-        :returns: the return type of this Routine.
-        :rtype: :py:class:`psyclone.psyir.symbols.DataType` or NoneType
+        Setter for the return-symbol of this Routine node.
+
+        :param value: the symbol holding the value that the routine returns.
+        :type value: :py:class:`psyclone.psyir.symbols.DataSymbol`
+
+        :raises TypeError: if the supplied value is not a DataSymbol.
+        :raises KeyError: if the supplied symbol is not a local entry in the \
+                          symbol table of this Routine.
         '''
-        return self._return_type
+        if not isinstance(value, DataSymbol):
+            raise TypeError("Routine return-symbol should be a DataSymbol "
+                            "but found '{0}'".format(
+                                type(value).__name__))
+        if value not in self.symbol_table.local_datasymbols:
+            raise KeyError(
+                "For a symbol to be a return-symbol, it must be present in "
+                "the symbol table of the Routine but '{0}' is not.".format(
+                    value.name))
+        self._return_symbol = value
 
 
 # For automatic documentation generation


Test patch:
diff --git a/src/psyclone/tests/psyir/backend/fortran_test.py b/src/psyclone/tests/psyir/backend/fortran_test.py
index c605b3417..b9c9d35ec 100644
--- a/src/psyclone/tests/psyir/backend/fortran_test.py
+++ b/src/psyclone/tests/psyir/backend/fortran_test.py
@@ -1017,6 +1017,34 @@ def test_fw_routine_program(fortran_reader, fortran_writer, tmpdir):
     assert Compile(tmpdir).string_compiles(result)
 
 
+def test_fw_routine_function(fortran_reader, fortran_writer, tmpdir):
+    ''' Check that the FortranWriter outputs a function when a routine node
+    is found with return_symbol set.
+
+    '''
+    code = ("module test\n"
+            "implicit none\n"
+            "real :: a\n"
+            "contains\n"
+            "function tmp(b) result(val)\n"
+            "  real :: val\n"
+            "  real :: b\n"
+            "  val = a + b\n"
+            "end function tmp\n"
+            "end module test")
+    container = fortran_reader.psyir_from_source(code)
+    # Generate Fortran from PSyIR
+    result = fortran_writer(container)
+    assert(
+        "  contains\n"
+        "  function tmp(b) result(val)\n"
+        "    real, intent(inout) :: b\n"
+        "    real :: val\n\n"
+        "    val = a + b\n\n"
+        "  end function tmp\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_derived_type_test.py b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py
index b89c509e3..bbd45a2c0 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py
@@ -103,9 +103,9 @@ def test_name_clash_derived_type(f2008_parser, type_name):
     # already contain a RoutineSymbol named 'my_type'
     with pytest.raises(SymbolError) as err:
         processor.process_declarations(fake_parent, fparser2spec.content, [])
-    assert ("Search for a TypeSymbol named '{0}' (required by declaration"
-            " 'TYPE({0}) :: some_var') found a 'RoutineSymbol' "
-            "instead".format(type_name) in str(err.value))
+    assert ("Search for a TypeSymbol named '{0}' (required by specification"
+            " 'TYPE({0})') found a 'RoutineSymbol' instead".format(type_name)
+            in str(err.value))
 
 
 def test_name_clash_derived_type_def(f2008_parser):
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 549f9fcaf..334daf2bd 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_generate_psyir_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_generate_psyir_test.py
@@ -91,19 +91,22 @@ FUNCTION_IN = (
     "integer function tmp(a)\n"
     "real :: a\n"
     "a=0.0\n"
+    "tmp = a\n"
     "end function tmp")
 FUNCTION_OUT = (
-    "INTEGER FUNCTION tmp(a)\n"
-    "  REAL :: a\n"
+    "function tmp(a)\n"
+    "  real, intent(inout) :: a\n"
+    "  integer :: tmp\n\n"
     "  a = 0.0\n"
-    "END FUNCTION tmp")
+    "  tmp = a\n\n"
+    "end function tmp\n")
 
 
 @pytest.mark.parametrize("code,expected,node_class",
                          [(MODULE_IN, MODULE_OUT, Container),
                           (SUB_IN, SUB_OUT, Routine),
                           (PROGRAM_IN, PROGRAM_OUT, Routine),
-                          (FUNCTION_IN, FUNCTION_OUT, CodeBlock)])
+                          (FUNCTION_IN, FUNCTION_OUT, Routine)])
 def test_generate_psyir(parser, code, expected, node_class):
     '''Test that generate_psyir generates PSyIR from an fparser2 parse
     tree.
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py b/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py
index b71daf4e3..9f27c95aa 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py
@@ -31,20 +31,22 @@
 # 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
+# Authors: R. W. Ford, STFC Daresbury Lab
+#          A. R. Porter, STFC Daresbury Lab
 
 '''Module containing pytest tests for the _subroutine_handler method
 in the class Fparser2Reader. This handler deals with the translation
-of the fparser2 Subroutine_Subprogram construct to PSyIR.
+of the fparser2 Subroutine_Subprogram and Function_Subprogram constructs
+to PSyIR.
 
 '''
 from __future__ import absolute_import
 import pytest
 
 from fparser.common.readfortran import FortranStringReader
-from psyclone.psyir.nodes import Routine
+from psyclone.psyir.symbols import DataSymbol, ScalarType
+from psyclone.psyir.nodes import Container, Routine, CodeBlock
 from psyclone.psyir.frontend.fparser2 import Fparser2Reader
-from psyclone.psyir.backend.fortran import FortranWriter
 
 # subroutine no declarations
 SUB1_IN = (
@@ -79,7 +81,7 @@ SUB3_OUT = (
                          [(SUB1_IN, SUB1_OUT),
                           (SUB2_IN, SUB2_OUT),
                           (SUB3_IN, SUB3_OUT)])
-def test_subroutine_handler(parser, code, expected):
+def test_subroutine_handler(parser, fortran_writer, code, expected):
     '''Test that subroutine_handler handles valid Fortran subroutines.'''
 
     processor = Fparser2Reader()
@@ -90,6 +92,168 @@ def test_subroutine_handler(parser, code, expected):
     # Check the expected PSyIR nodes are being created
     assert isinstance(psyir, Routine)
     assert psyir.parent is None
-    writer = FortranWriter()
-    result = writer(psyir)
+    result = fortran_writer(psyir)
     assert expected == result
+
+
+def test_function_handler(fortran_reader, fortran_writer):
+    '''Test that subroutine_handler correctly handles a function defined
+    within a module.
+
+    '''
+    code = (
+        "module a\n"
+        "contains\n"
+        "  function my_func()\n"
+        "    integer :: my_func\n"
+        "    my_func = 1\n"
+        "  end function my_func\n"
+        "end module\n")
+    expected = (
+        "module a\n"
+        "  implicit none\n\n"
+        "  public :: my_func\n\n"
+        "  contains\n"
+        "  function my_func()\n"
+        "    integer :: my_func\n"
+        "\n"
+        "    my_func = 1\n"
+        "\n"
+        "  end function my_func\n"
+        "\n"
+        "end module a\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    # Check PSyIR nodes are being created
+    assert isinstance(psyir, Container)
+    routines = psyir.walk(Routine)
+    assert len(routines) == 1
+    assert isinstance(routines[0].return_symbol, DataSymbol)
+    assert routines[0].return_symbol.name == "my_func"
+    assert (routines[0].return_symbol.datatype.intrinsic ==
+            ScalarType.Intrinsic.INTEGER)
+    assert psyir.parent is None
+    result = fortran_writer(psyir)
+    assert result == expected
+
+
+@pytest.mark.parametrize("basic_type", ["real", "integer"])
+def test_function_type_prefix(fortran_reader, fortran_writer, basic_type):
+    '''
+    Test the handler when the function definition has a type prefix but no
+    result suffix.
+
+    '''
+    code = (
+        "module a\n"
+        "contains\n"
+        "  {0} function my_func()\n"
+        "    my_func = 1\n"
+        "  end function my_func\n"
+        "end module\n".format(basic_type))
+    expected = (
+        "module a\n"
+        "  implicit none\n\n"
+        "  public :: my_func\n\n"
+        "  contains\n"
+        "  function my_func()\n"
+        "    {0} :: my_func\n"
+        "\n"
+        "    my_func = 1\n"
+        "\n"
+        "  end function my_func\n"
+        "\n"
+        "end module a\n".format(basic_type))
+    psyir = fortran_reader.psyir_from_source(code)
+    assert isinstance(psyir, Container)
+    assert isinstance(psyir.children[0], Routine)
+    return_sym = psyir.children[0].return_symbol
+    assert isinstance(return_sym, DataSymbol)
+    if basic_type == "real":
+        assert return_sym.datatype.intrinsic == ScalarType.Intrinsic.REAL
+    else:
+        assert return_sym.datatype.intrinsic == ScalarType.Intrinsic.INTEGER
+    result = fortran_writer(psyir)
+    assert result == expected
+
+
+FN1_IN = ("  function my_func() result(my_val)\n"
+          "    real :: my_val\n"
+          "    my_val = 1.0\n"
+          "  end function my_func\n")
+FN2_IN = ("  real function my_func() result(my_val)\n"
+          "    my_val = 1.0\n"
+          "  end function my_func\n")
+FN3_IN = ("  real(wp) function my_func() result(my_val)\n"
+          "    my_val = 1.0\n"
+          "  end function my_func\n")
+EXPECTED_FN_OUT = ("  function my_func() result(my_val)\n"
+                   "    real :: my_val\n\n"
+                   "    my_val = 1.0\n\n"
+                   "  end function my_func\n")
+EXPECTED_FN3_OUT = ("  function my_func() result(my_val)\n"
+                    "    real(kind=wp) :: my_val\n\n"
+                    "    my_val = 1.0\n\n"
+                    "  end function my_func\n")
+
+
+@pytest.mark.parametrize("code,expected",
+                         [(FN1_IN, EXPECTED_FN_OUT),
+                          (FN2_IN, EXPECTED_FN_OUT),
+                          (FN3_IN, EXPECTED_FN3_OUT)])
+def test_function_result_suffix(fortran_reader, fortran_writer,
+                                code, expected):
+    '''
+    Test that we handle a Fortran function with the return value specified
+    using the 'result()' suffix. We test when the type is specified by a
+    declaration inside the function or by a type specifier in the function
+    prefix.
+
+    '''
+    code = (
+        "module a\n"
+        "use kind_params, only: wp\n"
+        "contains\n"
+        "{0}end module\n".format(code))
+    psyir = fortran_reader.psyir_from_source(code)
+    # Check PSyIR nodes are being created
+    assert isinstance(psyir, Container)
+    routines = psyir.walk(Routine)
+    assert len(routines) == 1
+    assert (routines[0].return_symbol is
+            routines[0].symbol_table.lookup("my_val"))
+    result = fortran_writer(psyir)
+    assert expected in result
+
+
+def test_function_missing_return_type(fortran_reader):
+    '''
+    Test that we generate a CodeBlock for a Fortran function without an
+    explicit declaration of its return type (i.e. if it's relying on Fortran's
+    implicit typing).
+
+    '''
+    code = (
+        "module a\n"
+        "contains\n"
+        "  function my_func()\n"
+        "    my_func = 1.0\n"
+        "  end function my_func\n"
+        "end module\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    assert isinstance(psyir.children[0], CodeBlock)
+
+
+@pytest.mark.parametrize("fn_prefix",
+                         ["pure real", "real pure", "recursive", "elemental"])
+def test_unsupported_function_prefix(fortran_reader, fn_prefix):
+    ''' Check that we get a CodeBlock if a Fortran function has an unsupported
+    prefix. '''
+    code = (
+        "module a\n"
+        "contains\n"
+        "  {0} function my_func()\n"
+        "    my_func = 1.0\n"
+        "  end function my_func\n"
+        "end module\n".format(fn_prefix))
+    psyir = fortran_reader.psyir_from_source(code)
+    assert isinstance(psyir.children[0], CodeBlock)
diff --git a/src/psyclone/tests/psyir/nodes/kernel_schedule_test.py b/src/psyclone/tests/psyir/nodes/kernel_schedule_test.py
index aaeee636d..5ff5d481d 100644
--- a/src/psyclone/tests/psyir/nodes/kernel_schedule_test.py
+++ b/src/psyclone/tests/psyir/nodes/kernel_schedule_test.py
@@ -53,7 +53,7 @@ def test_kernelschedule_constructor():
     # A KernelSchedule does not represent a program
     assert not ksched.is_program
     # A KernelSchedule does not return anything
-    assert ksched.return_type is None
+    assert ksched.return_symbol is None
     assert ksched.parent is None
     # Now create a KernelSchedule with a parent
     cnode = Container("BigBox")
@@ -81,9 +81,9 @@ def test_kernelschedule_create():
                                    Literal("0.0", REAL_TYPE))
     kschedule = KernelSchedule.create("mod_name", symbol_table, [assignment])
     assert isinstance(kschedule, KernelSchedule)
-    # A KernelSchedule is not a main program and has no return type.
+    # A KernelSchedule is not a main program and has no return value.
     assert not kschedule.is_program
-    assert kschedule.return_type is None
+    assert kschedule.return_symbol is None
     check_links(kschedule, [assignment])
     assert kschedule.symbol_table is symbol_table
     result = FortranWriter().routine_node(kschedule)
diff --git a/src/psyclone/tests/psyir/nodes/routine_test.py b/src/psyclone/tests/psyir/nodes/routine_test.py
index b6ddaa1c3..8f2e9fc33 100644
--- a/src/psyclone/tests/psyir/nodes/routine_test.py
+++ b/src/psyclone/tests/psyir/nodes/routine_test.py
@@ -39,7 +39,7 @@
 from __future__ import absolute_import
 import pytest
 from psyclone.psyir.nodes import Routine, Assignment, Reference, Literal
-from psyclone.psyir.symbols import (INTEGER_TYPE, REAL_TYPE, DataSymbol,
+from psyclone.psyir.symbols import (REAL_TYPE, DataSymbol,
                                     SymbolTable, RoutineSymbol)
 from psyclone.tests.utilities import check_links
 
@@ -52,9 +52,6 @@ def test_routine_constructor():
     with pytest.raises(TypeError) as err:
         Routine("hello", is_program=1)
     assert "'is_program' must be a bool" in str(err.value)
-    with pytest.raises(TypeError) as err:
-        Routine("hello", return_type=1)
-    assert "'return_type' must be of type DataType" in str(err.value)
     node = Routine("hello")
     assert node._name == "hello"
 
@@ -63,25 +60,19 @@ def test_routine_properties():
     ''' Check the various properties of the Routine class. '''
     node1 = Routine("hello")
     assert node1.dag_name == "routine_hello_0"
-    assert node1.return_type is None
+    assert node1.return_symbol is None
     assert node1.is_program is False
     assert node1.name == "hello"
     # Give the Routine a child to get full coverage of __str__ method
     node1.addchild(Assignment())
     assert "Routine[name:'hello']:\nAssignment" in str(node1)
 
-    node2 = Routine("bonjour", return_type=INTEGER_TYPE)
-    assert node2.return_type == INTEGER_TYPE
+    node2 = Routine("bonjour")
     assert node2.is_program is False
 
     node3 = Routine("gutentag", is_program=True)
-    assert node3.return_type is None
     assert node3.is_program
 
-    node4 = Routine("welcome", is_program=True, return_type=INTEGER_TYPE)
-    assert node4.return_type == INTEGER_TYPE
-    assert node4.is_program
-
 
 def test_routine_name_setter():
     ''' Check the name setter property of the Routine class updates its
@@ -107,6 +98,28 @@ def test_routine_name_setter():
         "goodbye"
 
 
+def test_routine_return_symbol_setter():
+    ''' Check that the return_symbol setter works correctly and rejects invalid
+    values.
+
+    '''
+    node = Routine("hello")
+    assert node.return_symbol is None
+    with pytest.raises(TypeError) as err:
+        node.return_symbol = "wrong"
+    assert ("Routine return-symbol should be a DataSymbol but found 'str'" in
+            str(err.value))
+    sym = DataSymbol("result", REAL_TYPE)
+    with pytest.raises(KeyError) as err:
+        node.return_symbol = sym
+    assert ("For a symbol to be a return-symbol, it must be present in the "
+            "symbol table of the Routine but 'result' is not." in
+            str(err.value))
+    node.symbol_table.add(sym)
+    node.return_symbol = sym
+    assert node.return_symbol is sym
+
+
 def test_routine_create_invalid():
     '''Test that the create method in the Routine class raises the
     expected exceptions if the provided input is invalid.
@@ -143,6 +156,12 @@ def test_routine_create_invalid():
         "child of children argument in create method of Routine class "
         "should be a PSyIR Node but found 'str'." in str(excinfo.value))
 
+    # return_symbol is not a Symbol
+    with pytest.raises(TypeError) as excinfo:
+        _ = Routine.create("mod_name", symbol_table, [], return_symbol="wrong")
+    assert ("Routine return-symbol should be a DataSymbol but found 'str'" in
+            str(excinfo.value))
+
 
 def test_routine_create():
     '''Test that the create method correctly creates a Routine instance. '''
@@ -152,10 +171,9 @@ def test_routine_create():
     assignment = Assignment.create(Reference(symbol),
                                    Literal("0.0", REAL_TYPE))
     kschedule = Routine.create("mod_name", symbol_table, [assignment],
-                               is_program=True, return_type=INTEGER_TYPE)
+                               is_program=True, return_symbol=symbol)
     assert isinstance(kschedule, Routine)
     check_links(kschedule, [assignment])
     assert kschedule.symbol_table is symbol_table
     assert kschedule.is_program
-    assert kschedule.return_type == INTEGER_TYPE
-    # TODO #910 test the Fortran backend for the Routine node.
+    assert kschedule.return_symbol is symbol