Repository: stfc/fparser

Issue:
Allow for overriding of intrinsics with different numbers of arguments
If code shadows an intrinsic but gives it a different interface then fparser currently raises a SyntaxError as the interface does not match that of the Fortran intrinsic and it does not countenance any other options.
It would probably be better if the match on the intrinsic were simply allowed to fail at this stage.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38da79a..01cf9b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,8 @@ Modifications by (in alphabetical order):
 * P. Vitt, University of Siegen, Germany
 * A. Voysey, UK Met Office
 
+25/01/2024 PR #418 for #313. Allow intrinsic shadowing and improve fparser symbol table.
+
 11/01/2024 PR #439 for #432. Fix RTD build and clean up setuptools config.
 
 03/10/2023 PR #431 for #430. Fixes bug in WHERE handling in fparser1.
diff --git a/doc/source/developers_guide.rst b/doc/source/developers_guide.rst
index f1cf230..c58cf22 100644
--- a/doc/source/developers_guide.rst
+++ b/doc/source/developers_guide.rst
@@ -248,6 +248,7 @@ corresponding dictionary entries are instances of the `SymbolTable`
 class:
 
 .. autoclass:: fparser.two.symbol_table.SymbolTable
+   :members:
 
 The entries in these tables are instances of the named tuple,
 `SymbolTable.Symbol` which currently has the properties:
diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py
index 7cd4f96..2832bfb 100644
--- a/src/fparser/two/Fortran2003.py
+++ b/src/fparser/two/Fortran2003.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-# 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
 
@@ -12333,77 +12333,85 @@ class Intrinsic_Function_Reference(CallBase):  # No explicit rule
 
         :param str string: the string to match with the pattern rule.
 
-        :return: a tuple of size 2 containing the name of the \
-        intrinsic and its arguments if there is a match, or None if \
-        there is not.
-        :rtype: (:py:class:`fparser.two.Fortran2003.Intrinsic_Name`, \
-        :py:class:`fparser.two.Fortran2003.Actual_Arg_Spec_List`) or \
-        NoneType
+        :return: a tuple of size 2 containing the name of the intrinsic
+            and its arguments if there is a match, or None if there is not.
+        :rtype: Tuple[:py:class:`fparser.two.Fortran2003.Intrinsic_Name`,
+            :py:class:`fparser.two.Fortran2003.Actual_Arg_Spec_List`] | NoneType
 
-        :raises InternalSyntaxError: If the number of arguments \
-        provided does not match the number of arguments expected by \
-        the intrinsic.
+        :raises InternalSyntaxError: If the number of arguments provided does
+            not match the number of arguments expected by the intrinsic and
+            there are no wildcard imports that could be bringing a routine
+            (that overrides it) into scope.
 
         """
         result = CallBase.match(Intrinsic_Name, Actual_Arg_Spec_List, string)
-        if result:
-            # There is a match so check the number of args provided
-            # matches the number of args expected by the intrinsic.
-            function_name = str(result[0])
-            function_args = result[1]
+        if not result:
+            return None
 
-            # Check that that this name is not being shadowed (i.e. overridden)
-            # by a symbol in scope at this point.
-            table = SYMBOL_TABLES.current_scope
-            try:
-                table.lookup(function_name)
-                # We found a matching name so refuse to match this intrinsic.
-                return None
-            except (KeyError, AttributeError):
-                # There is either no matching name in the table or we have
-                # no current scoping region.
-                pass
-
-            # This if/else will not be needed once issue #170 has been
-            # addressed.
-            if isinstance(function_args, Actual_Arg_Spec_List):
-                nargs = len(function_args.items)
-            elif function_args is None:
-                nargs = 0
-            else:
-                nargs = 1
+        # There is a match so check the number of args provided
+        # matches the number of args expected by the intrinsic.
+        function_name = str(result[0])
+        function_args = result[1]
+
+        # Check that that this name is not being shadowed (i.e. overridden)
+        # by a symbol in scope at this point.
+        table = SYMBOL_TABLES.current_scope
+        try:
+            table.lookup(function_name)
+            # We found a matching name so refuse to match this intrinsic.
+            return None
+        except (KeyError, AttributeError):
+            # There is either no matching name in the table or we have
+            # no current scoping region.
+            pass
+
+        nargs = 0 if function_args is None else len(function_args.items)
+
+        if function_name in Intrinsic_Name.specific_function_names.keys():
+            # If this is a specific function then use its generic
+            # name to test min and max number of arguments.
+            test_name = Intrinsic_Name.specific_function_names[function_name]
+        else:
+            test_name = function_name
+
+        min_nargs = Intrinsic_Name.generic_function_names[test_name]["min"]
+        max_nargs = Intrinsic_Name.generic_function_names[test_name]["max"]
+
+        # None indicates an unlimited number of arguments
+        if max_nargs is None:
+            if nargs < min_nargs:
+                if table and not table.all_symbols_resolved:
+                    # Wrong number of arguments to be an intrinsic so it must
+                    # be a call to a routine being brought into scope from
+                    # elsewhere.
+                    return None
 
-            if function_name in Intrinsic_Name.specific_function_names.keys():
-                # If this is a specific function then use its generic
-                # name to test min and max number of arguments.
-                test_name = Intrinsic_Name.specific_function_names[function_name]
-            else:
-                test_name = function_name
-
-            min_nargs = Intrinsic_Name.generic_function_names[test_name]["min"]
-            max_nargs = Intrinsic_Name.generic_function_names[test_name]["max"]
-
-            if max_nargs is None:
-                if nargs < min_nargs:
-                    # None indicates an unlimited number of arguments
-                    raise InternalSyntaxError(
-                        "Intrinsic '{0}' expects at least {1} args but found "
-                        "{2}.".format(function_name, min_nargs, nargs)
-                    )
-                # The number of arguments is valid. Return here as
-                # further tests will fail due to max_args being
-                # None.
-                return result
-            if min_nargs == max_nargs and nargs != min_nargs:
-                raise InternalSyntaxError(
-                    "Intrinsic '{0}' expects {1} arg(s) but found {2}."
-                    "".format(function_name, min_nargs, nargs)
-                )
-            if min_nargs < max_nargs and (nargs < min_nargs or nargs > max_nargs):
                 raise InternalSyntaxError(
-                    "Intrinsic '{0}' expects between {1} and {2} args but "
-                    "found {3}.".format(function_name, min_nargs, max_nargs, nargs)
+                    "Intrinsic '{0}' expects at least {1} args but found "
+                    "{2}.".format(function_name, min_nargs, nargs)
                 )
+            # The number of arguments is valid. Return here as
+            # further tests will fail due to max_args being
+            # None.
+            return result
+        if min_nargs == max_nargs and nargs != min_nargs:
+            if table and not table.all_symbols_resolved:
+                return None
+            raise InternalSyntaxError(
+                "Intrinsic '{0}' expects {1} arg(s) but found {2}."
+                "".format(function_name, min_nargs, nargs)
+            )
+        if min_nargs < max_nargs and (nargs < min_nargs or nargs > max_nargs):
+            if table and not table.all_symbols_resolved:
+                # Wrong number of arguments to be an intrinsic so it must
+                # be a call to a routine being brought into scope from
+                # elsewhere.
+                return None
+
+            raise InternalSyntaxError(
+                "Intrinsic '{0}' expects between {1} and {2} args but "
+                "found {3}.".format(function_name, min_nargs, max_nargs, nargs)
+            )
         return result
 
 
diff --git a/src/fparser/two/symbol_table.py b/src/fparser/two/symbol_table.py
index fc2c32a..feed199 100644
--- a/src/fparser/two/symbol_table.py
+++ b/src/fparser/two/symbol_table.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2021-2022, Science and Technology Facilities Council.
+# Copyright (c) 2021-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -87,17 +87,19 @@ class SymbolTables:
         self._symbol_tables = {}
         self._current_scope = None
 
-    def add(self, name):
+    def add(self, name, node=None):
         """
         Add a new symbol table with the supplied name. The name will be
         converted to lower case if necessary.
 
         :param str name: the name for the new table.
+        :param node: the node in the parse tree associated with this table.
+        :type node: Optional[:py:class:`fparser.two.utils.Base`]
 
         :returns: the new symbol table.
         :rtype: :py:class:`fparser.two.symbol_table.SymbolTable`
 
-        :raises SymbolTableError: if there is already an entry with the \
+        :raises SymbolTableError: if there is already an entry with the
                                   supplied name.
         """
         lower_name = name.lower()
@@ -106,7 +108,7 @@ class SymbolTables:
                 f"The table of top-level (un-nested) symbol tables already "
                 f"contains an entry for '{lower_name}'"
             )
-        table = SymbolTable(lower_name, checking_enabled=self._enable_checks)
+        table = SymbolTable(lower_name, checking_enabled=self._enable_checks, node=node)
         self._symbol_tables[lower_name] = table
         return table
 
@@ -131,7 +133,7 @@ class SymbolTables:
         """
         return self._current_scope
 
-    def enter_scope(self, name):
+    def enter_scope(self, name, node=None):
         """
         Called when the parser enters a new scoping region (i.e. when it
         encounters one of the classes listed in `_scoping_unit_classes`).
@@ -142,7 +144,8 @@ class SymbolTables:
         bottom.
 
         :param str name: name of the scoping region.
-
+        :param node: the node of the parse tree associated with this region.
+        :type node: Optional[:py:class:`fparser.two.utils.Base`]
         """
         lname = name.lower()
 
@@ -152,12 +155,15 @@ class SymbolTables:
                 table = self.lookup(lname)
             except KeyError:
                 # Create a new, top-level symbol table with the supplied name.
-                table = self.add(lname)
+                table = self.add(lname, node=node)
         else:
             # We are already inside a scoping region so create a new table
             # and setup its parent/child connections.
             table = SymbolTable(
-                lname, parent=self._current_scope, checking_enabled=self._enable_checks
+                lname,
+                parent=self._current_scope,
+                checking_enabled=self._enable_checks,
+                node=node,
             )
             self._current_scope.add_child(table)
 
@@ -413,16 +419,26 @@ class ModuleUse:
     @property
     def symbol_names(self):
         """
-        :returns: the names of all symbols associated with USE(s) of this \
+        :returns: the names of all symbols associated with USE(s) of this
                   module.
         :rtype: List[str]
         """
         return list(self._symbols.keys())
 
+    def lookup(self, name):
+        """
+        :returns: the symbol with the supplied name imported from this module (if any).
+        :rtype: :py:class:`fparser.two.symbol_table.SymbolTable.Symbol`
+
+        :raises KeyError: if no symbol with the supplied name is imported from
+                          this module into the current scope.
+        """
+        return self._symbols[name.lower()]
+
     @property
     def only_list(self):
         """
-        :returns: the local names that appear in an Only_List or None if there \
+        :returns: the local names that appear in an Only_List or None if there
                   is no such list.
         :rtype: Optional[List[str]]
         """
@@ -433,7 +449,7 @@ class ModuleUse:
     @property
     def rename_list(self):
         """
-        :returns: the local names that appear in a Rename_List or None if there \
+        :returns: the local names that appear in a Rename_List or None if there
                   is no such list.
         :rtype: Optional[List[str]]
         """
@@ -466,13 +482,16 @@ class SymbolTable:
     Once #201 is complete it is planned to switch this so that the checks
     are instead enabled by default.
 
-    :param str name: the name of this scope. Will be the name of the \
-                     associated module or routine.
+    :param str name: the name of this scope. Will be the name of the
+        associated module or routine.
     :param parent: the symbol table within which this one is nested (if any).
     :type parent: :py:class:`fparser.two.symbol_table.SymbolTable.Symbol`
-    :param bool checking_enabled: whether or not validity checks are \
+    :param bool checking_enabled: whether or not validity checks are
         performed for symbols added to the table.
+    :param node: the node in the parse tree associated with this table.
+    :type node: Optional[:py:class:`fparser.two.utils.Base`]
 
+    :raises TypeError: if the supplied node is of the wrong type.
     """
 
     # TODO #201 add support for other symbol properties (kind, shape
@@ -480,7 +499,7 @@ class SymbolTable:
     # type checking for the various properties.
     Symbol = namedtuple("Symbol", "name primitive_type")
 
-    def __init__(self, name, parent=None, checking_enabled=False):
+    def __init__(self, name, parent=None, checking_enabled=False, node=None):
         self._name = name.lower()
         # Symbols defined in this scope that represent data.
         self._data_symbols = {}
@@ -491,6 +510,16 @@ class SymbolTable:
         # value (if any) is set via setter method.
         self._parent = None
         self.parent = parent
+        # The node in the parse tree with which this table is associated (if any).
+        from fparser.two.utils import Base
+
+        if node and not isinstance(node, Base):
+            raise TypeError(
+                f"The 'node' argument to the SymbolTable constructor must be a "
+                f"valid parse tree node (instance of utils.Base) but got "
+                f"'{type(node).__name__}'"
+            )
+        self._node = node
         # Whether or not to perform validity checks when symbols are added.
         self._checking_enabled = checking_enabled
         # Symbol tables nested within this one.
@@ -603,8 +632,16 @@ class SymbolTable:
         # Fortran is not case sensitive so convert input to lowercase.
         lname = name.lower()
         if lname in self._data_symbols:
+            # Found a match in this table.
             return self._data_symbols[lname]
-        # No match in this scope - search in parent scope (if any)
+        for module in self._modules.values():
+            try:
+                # Look to see whether the symbol is imported into this table.
+                return module.lookup(lname)
+            except KeyError:
+                pass
+        # No match in this scope - search in parent scope (if any). This will
+        # recurse upwards through parent tables as necessary.
         if self.parent:
             return self.parent.lookup(lname)
         raise KeyError(f"Failed to find symbol named '{lname}'")
@@ -644,6 +681,14 @@ class SymbolTable:
             )
         self._parent = value
 
+    @property
+    def node(self):
+        """
+        :returns: the scoping node (in the parse tree) asssociated with this SymbolTable.
+        :rtype: :py:class:`fparser.two.utils.Base`
+        """
+        return self._node
+
     def add_child(self, child):
         """
         Adds a child symbol table (scoping region nested within this one).
@@ -701,6 +746,49 @@ class SymbolTable:
             current = current.parent
         return current
 
+    @property
+    def wildcard_imports(self):
+        """
+        :returns: names of all modules with wildcard imports into this scope or an
+                  empty list if there are none.
+        :rtype: List[Optional[str]]
+        """
+        mod_names = set()
+        for mod_name, mod in self._modules.items():
+            if mod.wildcard_import:
+                mod_names.add(mod_name)
+        if self.parent:
+            # Any wildcard imports in a parent scope will affect this scoping
+            # region so carry on up. Note that if the root scoping region in
+            # the current file is a SUBMODULE then we will be missing whatever
+            # is brought into scope in the parent MODULE (since that will typically
+            # be in a separate source file).
+            mod_names.update(self.parent.wildcard_imports)
+
+        return sorted(list(mod_names))
+
+    @property
+    def all_symbols_resolved(self):
+        """
+        :returns: whether all symbols in this scope have been resolved. i.e. if
+            there are any wildcard imports or this table is within a submodule
+            then there could be symbols we don't have definitions for.
+        :rtype: bool
+        """
+        # wildcard_imports checks all parent scopes.
+        if self.wildcard_imports:
+            return False
+
+        # pylint: disable=import-outside-toplevel
+        from fparser.two.Fortran2008 import Submodule_Stmt
+
+        cursor = self
+        while cursor:
+            if isinstance(cursor.node, Submodule_Stmt):
+                return False
+            cursor = cursor.parent
+        return True
+
 
 #: The single, global container for all symbol tables constructed while
 #: parsing.
diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py
index 3c711ae..d9546d9 100644
--- a/src/fparser/two/utils.py
+++ b/src/fparser/two/utils.py
@@ -665,7 +665,7 @@ class BlockBase(Base):
                 # NOTE: if the match subsequently fails then we must
                 #       delete this symbol table.
                 table_name = obj.get_scope_name()
-                SYMBOL_TABLES.enter_scope(table_name)
+                SYMBOL_TABLES.enter_scope(table_name, obj)
             # Store the index of the start of this block proper (i.e.
             # excluding any comments)
             start_idx = len(content)


Test patch:
diff --git a/src/fparser/two/tests/fortran2003/test_intrinsics.py b/src/fparser/two/tests/fortran2003/test_intrinsics.py
index 3cdf51f..61938b8 100644
--- a/src/fparser/two/tests/fortran2003/test_intrinsics.py
+++ b/src/fparser/two/tests/fortran2003/test_intrinsics.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2019-2022, Science and Technology Facilities Council.
+# Copyright (c) 2019-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -61,7 +61,8 @@ def test_intrinsic_recognised():
     assert walk(ast, Intrinsic_Function_Reference)
 
 
-def test_intrinsic_error(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_error():
     """Test that Program raises the expected exception when there is an
     intrinsic syntax error.
 
@@ -77,21 +78,24 @@ def test_intrinsic_error(f2003_create):
 # class intrinsic_name
 
 
-def test_intrinsic_name_generic(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_name_generic():
     """Test that class Intrinsic_Name correctly matches a generic name."""
     result = Intrinsic_Name("COS")
     assert isinstance(result, Intrinsic_Name)
     assert str(result) == "COS"
 
 
-def test_intrinsic_name_specific(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_name_specific():
     """Test that class Intrinsic_Name correctly matches a specific name."""
     result = Intrinsic_Name("CCOS")
     assert isinstance(result, Intrinsic_Name)
     assert str(result) == "CCOS"
 
 
-def test_intrinsic_name_invalid(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_name_invalid():
     """Test that class Intrinsic_Name raises the expected exception if an
     invalid intrinsic name is provided.
 
@@ -100,7 +104,8 @@ def test_intrinsic_name_invalid(f2003_create):
         _ = Intrinsic_Name("NOT_AN_INTRINSIC")
 
 
-def test_intrinsic_name_case_insensitive(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_name_case_insensitive():
     """Test that class Intrinsic_Name is a case insensitive match which
     returns the name in upper case.
 
@@ -133,7 +138,8 @@ def test_intrinsic_function_reference_generic():
     SYMBOL_TABLES.exit_scope()
 
 
-def test_intrinsic_function_reference(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference():
     """Test that class Intrinsic_Function_Reference correctly matches a
     specific intrinsic with a valid number of arguments.
 
@@ -143,7 +149,8 @@ def test_intrinsic_function_reference(f2003_create):
     assert str(result) == "DSIN(A)"
 
 
-def test_intrinsic_function_nomatch(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_nomatch():
     """Test that class Intrinsic_Function_Reference raises the expected
     exception if there is no match.
 
@@ -152,7 +159,8 @@ def test_intrinsic_function_nomatch(f2003_create):
         _ = Intrinsic_Function_Reference("NO_MATCH(A)")
 
 
-def test_intrinsic_function_reference_multi_args(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference_multi_args():
     """Test that class Intrinsic_Function_Reference correctly matches a
     generic intrinsic which accepts more than one argument (two in
     this case).
@@ -163,7 +171,8 @@ def test_intrinsic_function_reference_multi_args(f2003_create):
     assert str(result) == "MATMUL(A, B)"
 
 
-def test_intrinsic_function_reference_zero_args(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference_zero_args():
     """Test that class Intrinsic_Function_Reference correctly matches a
     generic intrinsic which accepts zero arguments.
 
@@ -173,29 +182,32 @@ def test_intrinsic_function_reference_zero_args(f2003_create):
     assert str(result) == "COMMAND_ARGUMENT_COUNT()"
 
 
-def test_intrinsic_function_reference_range_args(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference_range_args():
     """Test that class Intrinsic_Function_Reference correctly matches a
     generic intrinsic which accepts a range of number of arguments.
 
     """
     for args in ["", "A", "A, B", "A, B, C"]:
-        result = Intrinsic_Function_Reference("SYSTEM_CLOCK({0})".format(args))
+        result = Intrinsic_Function_Reference(f"SYSTEM_CLOCK({args})")
         assert isinstance(result, Intrinsic_Function_Reference)
-        assert str(result) == "SYSTEM_CLOCK({0})".format(args)
+        assert str(result) == f"SYSTEM_CLOCK({args})"
 
 
-def test_intrinsic_function_reference_unlimited_args(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference_unlimited_args():
     """Test that class Intrinsic_Function_Reference correctly matches a
     generic intrinsic which accepts an unlimitednumber of arguments.
 
     """
     for args in ["A, B", "A, B, C", "A, B, C, D"]:
-        result = Intrinsic_Function_Reference("MAX({0})".format(args))
+        result = Intrinsic_Function_Reference(f"MAX({args})")
         assert isinstance(result, Intrinsic_Function_Reference)
-        assert str(result) == "MAX({0})".format(args)
+        assert str(result) == f"MAX({args})"
 
 
-def test_intrinsic_function_reference_error1(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference_error1():
     """Test that class Intrinsic_Function_Reference raises the expected
     exception when the valid min and max args are equal (2 in this case)
     and the wrong number of arguments is supplied.
@@ -210,7 +222,8 @@ def test_intrinsic_function_reference_error1(f2003_create):
     assert "Intrinsic 'MATMUL' expects 2 arg(s) but found 3." "" in str(excinfo.value)
 
 
-def test_intrinsic_function_reference_error2(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference_error2():
     """Test that class Intrinsic_Function_Reference raises the expected
     exception when the valid min args is less than the valid max args
     and the wrong number of arguments is supplied.
@@ -229,7 +242,8 @@ def test_intrinsic_function_reference_error2(f2003_create):
     )
 
 
-def test_intrinsic_function_reference_error3(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference_error3():
     """Test that class Intrinsic_Function_Reference raises the expected
     exception when the number of arguments is unlimited.
 
@@ -241,19 +255,20 @@ def test_intrinsic_function_reference_error3(f2003_create):
     )
 
 
-def test_intrinsic_inside_intrinsic(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_inside_intrinsic():
     """Test that when an intrinsic is within another instrinsic then both
     are recognised as intrinsics.
 
     """
-    reader = get_reader("subroutine sub()\na = sin(cos(b))\nend " "subroutine sub\n")
+    reader = get_reader("subroutine sub()\na = sin(cos(b))\nend subroutine sub\n")
     ast = Program(reader)
     rep = repr(ast).replace("u'", "'")
     assert "Intrinsic_Name('SIN')" in rep
     assert "Intrinsic_Name('COS')" in rep
 
 
-def test_shadowed_intrinsic(f2003_parser):
+def test_locally_shadowed_intrinsic(f2003_parser):
     """Check that a locally-defined symbol that shadows (overwrites) a
     Fortran intrinsic is correctly identified."""
     tree = f2003_parser(
@@ -277,3 +292,94 @@ end module my_mod
     table = tables.lookup("my_mod")
     sym = table.children[0].lookup("dot_product")
     assert sym.primitive_type == "real"
+
+
+def test_shadowed_intrinsic_named_import(f2003_parser):
+    """Check that an imported symbol that shadows (overwrites) a
+    Fortran intrinsic is correctly identified."""
+    tree = f2003_parser(
+        get_reader(
+            """\
+module my_mod
+  use some_mod, only: dot_product
+contains
+  subroutine my_sub()
+    real :: result
+    result = dot_product(1,1)
+  end subroutine my_sub
+end module my_mod
+    """
+        )
+    )
+    tables = SYMBOL_TABLES
+    # We should not have an intrinsic-function reference in the parse tree
+    assert not walk(tree, Intrinsic_Function_Reference)
+    table = tables.lookup("my_mod")
+    sym = table.children[0].lookup("dot_product")
+    assert sym.primitive_type == "unknown"
+
+
+@pytest.mark.parametrize("use_stmts", [("use some_mod", ""), ("", "use some_mod")])
+def test_shadowed_intrinsic_import(f2003_parser, use_stmts):
+    """Check that an imported symbol that shadows (overwrites) a Fortran
+    intrinsic is not identified as an intrinsic if it has the wrong
+    number of 'arguments'.
+
+    """
+    tree = f2003_parser(
+        get_reader(
+            f"""\
+module my_mod
+  {use_stmts[0]}\n
+contains
+  subroutine my_sub()
+    {use_stmts[1]}\n
+    real :: result
+    ! Too many args
+    result = dot_product(1,1,1)
+    ! Too few args for an intrinsic that has no max arg. count
+    result = max()
+    ! Wrong number of args for an intrinsic with a min and max arg. count that are
+    ! not equal.
+    result = aint(1, 2, 3)
+
+    contains
+
+    function tricky(a) result(b)
+      real, intent(in) :: a
+      real :: b
+      ! Another reference to dot_product for which we need to find the
+      ! wildcard import in the top-level module.
+      b = 2 * a + dot_product(a,1,2)
+    end function tricky
+  end subroutine my_sub
+end module my_mod
+    """
+        )
+    )
+    # We should not have an intrinsic-function reference in the parse tree
+    assert not walk(tree, Intrinsic_Function_Reference)
+
+
+def test_shadowed_intrinsic_error(f2003_parser):
+    """Check that an imported symbol that shadows (overwrites) a
+    Fortran intrinsic is not identified as an intrinsic if it has the wrong
+    types of argument. At the moment we are unable to check the types of
+    arguments (TODO #201) and so this test x-fails."""
+    tree = f2003_parser(
+        get_reader(
+            """\
+module my_mod
+  use some_mod
+contains
+  subroutine my_sub()
+    real :: result
+    result = dot_product(1,1)
+  end subroutine my_sub
+end module my_mod
+    """
+        )
+    )
+    # We should not have an intrinsic-function reference in the parse tree
+    if walk(tree, Intrinsic_Function_Reference):
+        pytest.xfail("TODO #201: incorrect match of Intrinsic_Function_Reference")
diff --git a/src/fparser/two/tests/fortran2008/test_intrinsics_2008.py b/src/fparser/two/tests/fortran2008/test_intrinsics_2008.py
new file mode 100644
index 0000000..68ab54e
--- /dev/null
+++ b/src/fparser/two/tests/fortran2008/test_intrinsics_2008.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2023 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 intrinsic handling within Fortran2008. At the moment, the only
+special consideration (beyond 2003) is that we can't disambiguate a
+shadowed intrinsic within a submodule.
+
+"""
+
+import pytest
+from fparser.api import get_reader
+from fparser.two import Fortran2003, Fortran2008
+from fparser.two.utils import walk
+
+
+@pytest.mark.usefixtures("f2008_create")
+def test_intrinsic_in_submodule():
+    """Test that a shadowed intrinsic within a submodule is accepted
+    (since a submodule shares the scope of its parent module and we
+    assume that is bringing the shadowed symbol into scope).
+
+    """
+    reader = get_reader(
+        """\
+      submodule (foobar) bar
+
+        contains
+        subroutine my_sub
+          real :: a
+          a = dot_product(1,2,3)
+        end subroutine my_sub
+      end
+      """
+    )
+    ast = Fortran2008.Submodule(reader)
+    assert not walk(ast, Fortran2003.Intrinsic_Function_Reference)
diff --git a/src/fparser/two/tests/test_module_use.py b/src/fparser/two/tests/test_module_use.py
index 96626d8..3aa04ff 100644
--- a/src/fparser/two/tests/test_module_use.py
+++ b/src/fparser/two/tests/test_module_use.py
@@ -1,5 +1,5 @@
 # -----------------------------------------------------------------------------
-# Copyright (c) 2022 Science and Technology Facilities Council.
+# Copyright (c) 2022-2023 Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Modifications made as part of the fparser project are distributed
@@ -160,3 +160,15 @@ def test_moduse_update():
     moduse6 = ModuleUse("flInt")
     moduse5.update(moduse6)
     assert moduse5.wildcard_import is True
+
+
+def test_moduse_lookup():
+    """Tests for the lookup() method."""
+    moduse = ModuleUse("flint")
+    with pytest.raises(KeyError):
+        moduse.lookup("polly")
+    moduse2 = ModuleUse("flint", only_list=[("mate", None), ("cannON", None)])
+    sym = moduse2.lookup("caNNon")
+    assert sym.name == "cannon"
+    # As this symbol is imported, we don't know its type.
+    assert sym.primitive_type == "unknown"
diff --git a/src/fparser/two/tests/test_symbol_table.py b/src/fparser/two/tests/test_symbol_table.py
index e619079..6c87386 100644
--- a/src/fparser/two/tests/test_symbol_table.py
+++ b/src/fparser/two/tests/test_symbol_table.py
@@ -1,5 +1,5 @@
 # -----------------------------------------------------------------------------
-# Copyright (c) 2021-2022 Science and Technology Facilities Council.
+# Copyright (c) 2021-2023 Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Modifications made as part of the fparser project are distributed
@@ -37,8 +37,10 @@
  of fparser2. """
 
 import pytest
-from fparser.two.symbol_table import SymbolTable, SYMBOL_TABLES, SymbolTableError
 from fparser.api import get_reader
+from fparser.two import Fortran2003
+from fparser.two.parser import ParserFactory
+from fparser.two.symbol_table import SymbolTable, SYMBOL_TABLES, SymbolTableError
 
 
 def test_basic_table():
@@ -48,6 +50,7 @@ def test_basic_table():
     assert table.name == "basic"
     assert table.parent is None
     assert table.children == []
+    assert table.node is None
     # Consistency checking is disabled by default
     assert table._checking_enabled is False
     with pytest.raises(KeyError) as err:
@@ -61,6 +64,15 @@ def test_basic_table():
     # Check that we can enable consistency checking
     table2 = SymbolTable("table2", checking_enabled=True)
     assert table2._checking_enabled is True
+    # Check that we can supply an associated parse tree node.
+    with pytest.raises(TypeError) as err:
+        SymbolTable("table3", node="oops")
+    assert (
+        "The 'node' argument to the SymbolTable constructor must be a "
+        "valid parse tree node (instance of utils.Base) but got 'str'" in str(err.value)
+    )
+    table3 = SymbolTable("table3", node=Fortran2003.Return_Stmt("return"))
+    assert isinstance(table3.node, Fortran2003.Return_Stmt)
 
 
 def test_add_data_symbol():
@@ -246,6 +258,12 @@ END PROGRAM a_prog
     assert table._modules["some_mod"].only_list == []
     assert "mod2" in table._modules
     assert sorted(table._modules["mod2"].only_list) == ["that_one", "this_one"]
+    sym = table.lookup("this_one")
+    assert sym.name == "this_one"
+    assert sym.primitive_type == "unknown"
+    sym = table.lookup("that_one")
+    assert sym.name == "that_one"
+    assert sym.primitive_type == "unknown"
 
 
 def test_module_use_with_rename(f2003_parser):
@@ -279,6 +297,73 @@ END PROGRAM a_prog
     assert table.lookup("other").primitive_type == "logical"
 
 
+def test_wildcard_module_search(f2003_parser):
+    """Test the wildcard_imports method of the SymbolTable."""
+    _ = f2003_parser(
+        get_reader(
+            """\
+module my_mod
+  use other_mod, only: b
+  use medium_mod
+  use some_mod
+  real :: a
+contains
+  subroutine sub
+    use big_mod
+    use medium_mod, only: c
+    use pointless_mod, only:
+  end subroutine sub
+end module my_mod
+    """
+        )
+    )
+    # Module symbol table should have two modules listed with wildcard imports.
+    mod_table = SYMBOL_TABLES.lookup("my_mod")
+    assert mod_table.wildcard_imports == ["medium_mod", "some_mod"]
+    # Move down to the subroutine and check that we recurse upwards.
+    sub_table = mod_table.children[0]
+    assert sub_table.wildcard_imports == ["big_mod", "medium_mod", "some_mod"]
+    # Repeat for a program containing a subroutine containing a function.
+    _ = f2003_parser(
+        get_reader(
+            """\
+program my_prog
+  use pointless_mod, only: dee
+  use medium_mod
+  use no_really_mod
+  real :: a
+  call sub()
+contains
+  subroutine sub
+    use big_mod
+    use no_really_mod, only: avon
+    use medium_mod, only: c
+    use pointless_mod, only:
+    write(*,*) "yes", my_fn()
+  contains
+    integer function my_fn()
+      use no_really_mod, only: afon
+      use tiny_mod
+      my_fn = 1 + afon
+    end function my_fn
+  end subroutine sub
+end program my_prog
+    """
+        )
+    )
+    prog_table = SYMBOL_TABLES.lookup("my_prog")
+    assert prog_table.wildcard_imports == ["medium_mod", "no_really_mod"]
+    sub_table = prog_table.children[0]
+    assert sub_table.wildcard_imports == ["big_mod", "medium_mod", "no_really_mod"]
+    fn_table = sub_table.children[0]
+    assert fn_table.wildcard_imports == [
+        "big_mod",
+        "medium_mod",
+        "no_really_mod",
+        "tiny_mod",
+    ]
+
+
 def test_module_definition(f2003_parser):
     """Check that a SymbolTable is created for a module and populated with
     the symbols it defines."""
@@ -296,6 +381,7 @@ end module my_mod
     assert list(tables._symbol_tables.keys()) == ["my_mod"]
     table = tables.lookup("my_mod")
     assert isinstance(table, SymbolTable)
+    assert isinstance(table.node, Fortran2003.Module_Stmt)
     assert "some_mod" in table._modules
     assert "a" in table._data_symbols
     sym = table.lookup("a")
@@ -322,6 +408,7 @@ end module my_mod
     tables = SYMBOL_TABLES
     assert list(tables._symbol_tables.keys()) == ["my_mod"]
     table = tables.lookup("my_mod")
+    assert isinstance(table.node, Fortran2003.Module_Stmt)
     assert len(table.children) == 1
     assert table.children[0].name == "my_sub"
     assert table.children[0].parent is table
@@ -352,6 +439,48 @@ end program my_prog
     assert list(tables._symbol_tables.keys()) == ["my_prog"]
     table = SYMBOL_TABLES.lookup("my_prog")
     assert len(table.children) == 1
+    assert isinstance(table.node, Fortran2003.Program_Stmt)
     assert table.children[0].name == "my_sub"
+    assert isinstance(table.children[0].node, Fortran2003.Subroutine_Stmt)
     assert table.children[0]._data_symbols["b"].name == "b"
     assert table.children[0].parent is table
+
+
+def test_all_symbols_resolved(f2003_parser):
+    """Tests for the all_symbols_resolved() method."""
+    code = """\
+program my_prog
+  use some_mod
+  real :: a
+contains
+  subroutine my_sub()
+    real :: b
+  end subroutine my_sub
+end program my_prog
+    """
+    _ = f2003_parser(get_reader(code))
+    table = SYMBOL_TABLES.lookup("my_prog").children[0]
+    assert table.all_symbols_resolved is False
+    new_code = code.replace("some_mod\n", "some_mod, only: igor\n")
+    SYMBOL_TABLES.clear()
+    _ = f2003_parser(get_reader(new_code))
+    table = SYMBOL_TABLES.lookup("my_prog").children[0]
+    assert table.all_symbols_resolved is True
+
+
+def test_all_symbols_resolved_submodule():
+    """Tests for the all_symbols_resolved() method returns False when the table
+    is within a Fortran2008 submodule (since a submodule has access to the
+    scope of its parent module)."""
+    code = """\
+submodule (other_mod) my_mod
+  real :: a
+contains
+  subroutine my_sub()
+    real :: b
+  end subroutine my_sub
+end submodule my_mod
+    """
+    _ = ParserFactory().create(std="f2008")(get_reader(code))
+    table = SYMBOL_TABLES.lookup("my_mod").children[0]
+    assert table.all_symbols_resolved is False