diff --git a/CHANGELOG.md b/CHANGELOG.md index 38da79a1..01cf9b69 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 f1cf2308..c58cf220 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 7cd4f96a..2832bfb9 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 @@ def match(string): :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 fc2c32a4..feed1994 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 @@ def clear(self): 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 @@ def add(self, name): 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 @@ def current_scope(self): """ 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 @@ def enter_scope(self, name): 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 @@ def enter_scope(self, name): 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 @@ def name(self): @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 @@ def only_list(self): @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 @@ def __init__(self, name, parent=None, checking_enabled=False): # 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 @@ def lookup(self, name): # 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 @@ def parent(self, value): ) 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 @@ def root(self): 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/tests/fortran2003/test_intrinsics.py b/src/fparser/two/tests/fortran2003/test_intrinsics.py index 3cdf51f8..61938b8a 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 @@ def test_shadowed_intrinsic(f2003_parser): 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 00000000..68ab54ee --- /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 96626d86..3aa04ff5 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 e619079e..6c873862 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 @@ def test_module_use_with_only(f2003_parser): 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 @@ def test_module_use_with_rename(f2003_parser): 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 @@ def test_module_definition(f2003_parser): 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 @@ def test_routine_in_module(f2003_parser): 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 @@ def test_routine_in_prog(f2003_parser): 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 diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 3c711aee..d9546d98 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -665,7 +665,7 @@ def match( # 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)