Source code for fparser.two.symbol_table

# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2021-2023, Science and Technology Facilities Council.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------

"""
The fparser2 symbol-table module. Defines various classes as well as
the single, global SYMBOL_TABLES instance. The latter is a container
for all of the top-level scoping units encountered during parsing.

"""
from collections import namedtuple


[docs] class SymbolTableError(Exception): """Base class exception for symbol-table related errors."""
[docs] class SymbolTables: """ Class encapsulating functionality for the global symbol-tables object. This is a container for all symbol tables constructed while parsing code. All names are converted to lower case (since Fortran is not case sensitive). """ def __init__(self): self._symbol_tables = {} # The symbol table of the current scope self._current_scope = None # Whether or not we enable consistency checks in the symbol tables # that are created. self._enable_checks = False
[docs] def __str__(self): result = ( f"SymbolTables: {len(self._symbol_tables)} tables\n" "========================\n" ) return result + "\n".join(sorted(self._symbol_tables.keys()))
[docs] def enable_checks(self, value): """ Sets whether or not to enable consistency checks in every symbol table that is created during a parse. :param bool value: whether or not checks are enabled. """ self._enable_checks = value
[docs] def clear(self): """ Deletes any stored SymbolTables. """ self._symbol_tables = {} self._current_scope = None
[docs] 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 supplied name. """ lower_name = name.lower() if lower_name in self._symbol_tables: raise SymbolTableError( 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, node=node) self._symbol_tables[lower_name] = table return table
[docs] def lookup(self, name): """ Find the named symbol table and return it. :param str name: the name of the required symbol table (not case \ sensitive). :returns: the named symbol table. :rtype: :py:class:`fparser.two.symbol_table.SymbolTable` """ return self._symbol_tables[name.lower()]
@property
[docs] def current_scope(self): """ :returns: the symbol table for the current scoping unit or None. :rtype: :py:class:`fparser.two.symbol_table.SymbolTable` or NoneType """ return self._current_scope
[docs] 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`). Sets the 'current scope' to be the symbol table with the supplied name. If we are not currently within a tree of scoping regions then a new entry is created in the internal dict of symbol tables. If there is an existing tree then a new table is created and added to the 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() if not self._current_scope: # We're not already inside a nested scope. try: table = self.lookup(lname) except KeyError: # Create a new, top-level symbol table with the supplied name. 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, node=node, ) self._current_scope.add_child(table) # Finally, make this new table the current scope self._current_scope = table
[docs] def exit_scope(self): """ Marks the end of the processing of the current scoping unit. Since we are exiting the current scoping region, the new 'current scoping region' will be its parent. :raises SymbolTableError: if there is no current scope from which to \ exit. """ if not self._current_scope: raise SymbolTableError("exit_scope() called but no current scope exists.") self._current_scope = self._current_scope.parent
[docs] def remove(self, name): """ Removes the named symbol table and any descendants it may have. When searching for the named table, the current scope takes priority followed by the list of top-level symbol tables. :param str name: the name of the symbol table to remove (not case \ sensitive). :raises SymbolTableError: if the named symbol table is not in the \ current scope or in the list of top-level symbol tables. """ lname = name.lower() if self._current_scope: try: self._current_scope.del_child(lname) # We succeeded in removing it from the current scope so we # are done. return except KeyError: pass if lname not in self._symbol_tables: msg = f"Failed to find a table named '{name}' in " if self._current_scope: msg += ( f"either the current scope (which contains " f"{[child.name for child in self._current_scope.children]}) or " ) msg += ( f"the list of top-level symbol tables " f"({list(self._symbol_tables.keys())})." ) raise SymbolTableError(msg) # Check that we're not currently somewhere inside the scope of the # named table. top_table = self._symbol_tables[lname] if self._current_scope: if self._current_scope.root is top_table: raise SymbolTableError( f"Cannot remove top-level symbol table '{name}' because the " f"current scope '{self._current_scope.name}' has it as an ancestor." ) del self._symbol_tables[lname]
[docs] class ModuleUse: """ Class capturing information on all USE statements referring to a given Fortran module. A USE statement can rename an imported symbol so as to avoid a clash in the local scope, e.g. `USE my_mod, alocal => amod` where `amod` is the name of the symbol declared in `my_mod`. This renaming can also occur inside an Only_List, e.g. `USE my_mod, only: alocal => amod`. :param str name: the name of the module. :param only_list: list of 2-tuples giving the (local-name, module-name) \ of symbols that appear in an Only_List. If a symbol is not re-named \ then module-name can be None. :type only_list: Optional[List[Tuple[str, str | NoneType]]] :param rename_list: list of 2-tuples given the (local-name, module-name) \ of symbols that appear in a Rename_List. :type rename_list: Optional[List[Tuple[str, str]]] :raises TypeError: if any of the supplied parameters are of the wrong type. """ def __init__(self, name, only_list=None, rename_list=None): if not isinstance(name, str): raise TypeError( f"The name of the module must be a str but got " f"'{type(name).__name__}'" ) self._validate_tuple_list("only", only_list) self._validate_tuple_list("rename", rename_list) if only_list and not all( isinstance(item[0], str) and (item[1] is None or isinstance(item[1], str)) for item in only_list ): raise TypeError( f"If present, the only_list must be a list of " f"2-tuples of (str, str | NoneType) but got: {only_list}" ) if rename_list and not all( isinstance(item[0], str) and isinstance(item[1], str) for item in rename_list ): raise TypeError( f"If present, the rename_list must be a list of " f"2-tuples of (str, str) but got: {rename_list}" ) self._name = name.lower() # dict of Symbols known to be accessed in this module. self._symbols = {} # Mapping from local symbol name in current scope to actual, declared # name in the module from which it is imported. self._local_to_module_map = {} if only_list is not None: self._store_symbols(only_list) self._wildcard_import = False self._only_set = set(local_name.lower() for local_name, _ in only_list) else: self._only_set = None self._wildcard_import = True if rename_list: self._store_symbols(rename_list) self._rename_set = set(local_name.lower() for local_name, _ in rename_list) else: self._rename_set = None @staticmethod def _validate_tuple_list(name, tlist): """ Validate the supplied list of tuples. :param str name: the name of the list being validated. :param tlist: the list of tuples to validate. :type tlist: Optional[List[Tuple[str, str | NoneType]]] :raises TypeError: if the supplied list is of the wrong type. """ if not tlist: # None or an empty list is fine. return if not isinstance(tlist, list): raise TypeError( f"If present, the {name}_list must be a list but " f"got '{type(tlist).__name__}'" ) if not all(isinstance(item, tuple) and len(item) == 2 for item in tlist): raise TypeError( f"If present, the {name}_list must be a list of " f"2-tuples but got: {tlist}" ) def _store_symbols(self, rename_list): """ Utility that updates the local list of symbols and renaming map for the given list of local-name, module-name tuples. If a symbol is not renamed then module-name can be None. :param rename_list: list of local-name, module-name pairs. :type rename_list: List[Tuple[str, str | NoneType]] """ for local_name, orig_name in rename_list: lname = local_name.lower() oname = orig_name.lower() if orig_name else None if lname not in self._symbols: self._symbols[lname] = SymbolTable.Symbol(lname, "unknown") if oname: self._local_to_module_map[lname] = oname def update(self, other): """ Update the current information with the information on the USE held in 'other'. :param other: the other Use instance from which to update this one. :type other: :py:class:`fparser.two.symbol_table.Use` :raises TypeError: if 'other' is of the wrong type. :raises ValueError: if 'other' is for a module with a different \ name to the current one. """ if not isinstance(other, ModuleUse): raise TypeError( f"update() must be supplied with an instance of " f"ModuleUse but got '{type(other).__name__}'" ) if self.name != other.name: raise ValueError( f"The ModuleUse supplied to update() is for module " f"'{other.name}' but this ModuleUse is for module " f"'{self.name}'" ) # Take the union of the only and rename lists and update the list # of symbols. if other.only_list: for local_name in other.only_list: if local_name not in self._symbols: self._symbols[local_name] = SymbolTable.Symbol( local_name, "unknown" ) # pylint: disable=protected-access if self._only_set is None: self._only_set = other._only_set else: self._only_set = self._only_set.union(other._only_set) # pylint: enable=protected-access if other.rename_list: for local_name in other.rename_list: if local_name not in self._symbols: self._symbols[local_name] = SymbolTable.Symbol( local_name, "unknown" ) # pylint: disable=protected-access if self._rename_set is None: self._rename_set = other._rename_set else: self._rename_set = self._rename_set.union(other._rename_set) # pylint: enable=protected-access # pylint: disable=protected-access self._local_to_module_map.update(other._local_to_module_map) # pylint: enable=protected-access self._wildcard_import = self._wildcard_import or other.wildcard_import @property def name(self): """ :returns: the name of the Fortran module. :rtype: str """ return self._name @property def symbol_names(self): """ :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 is no such list. :rtype: Optional[List[str]] """ if self._only_set is None: return None return list(self._only_set) @property def rename_list(self): """ :returns: the local names that appear in a Rename_List or None if there is no such list. :rtype: Optional[List[str]] """ if self._rename_set is None: return None return list(self._rename_set) @property def wildcard_import(self): """ :returns: whether there is a wildcard import from this module. :rtype: bool """ return self._wildcard_import def get_declared_name(self, name): """ :returns: the name of the supplied symbol as declared in the module. :rtype: str """ return self._local_to_module_map.get(name, name)
[docs] class SymbolTable: """ Class implementing a single symbol table. Since this functionality is not yet fully mature, checks that new symbols don't clash with existing symbols are disabled by default. 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 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 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 # and visibility). We may need a distinct Symbol class so as to provide # type checking for the various properties.
[docs] Symbol = namedtuple("Symbol", "name primitive_type")
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 = {} # dict of ModuleUse objects (indexed by module name) representing # modules imported into this scope. self._modules = {} # Reference to a SymbolTable that contains this one (if any). Actual # 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. self._children = []
[docs] def __str__(self): header = "===========\n" symbols = "Symbols:\n" if self._data_symbols: symbols += "\n".join(list(self._data_symbols.keys())) + "\n" uses = "Used modules:\n" if self._modules: uses += "\n".join(list(self._modules.keys())) + "\n" return f"{header}Symbol Table '{self._name}'\n{symbols}{uses}{header}"
[docs] def add_data_symbol(self, name, primitive_type): """ Creates a new Symbol with the specified properties and adds it to the symbol table. The supplied name is converted to lower case. TODO #201 add support for other symbol properties (kind, shape and visibility). :param str name: the name of the symbol. :param str primitive_type: the primitive type of the symbol. :raises TypeError: if any of the supplied parameters are of the \ wrong type. :raises SymbolTableError: if the symbol table already contains an entry with the supplied name. """ if not isinstance(name, str): raise TypeError( f"The name of the symbol must be a str but got " f"'{type(name).__name__}'" ) # TODO #201 use an enumeration for the primitive type if not isinstance(primitive_type, str): raise TypeError( f"The primitive type of the symbol must be specified as a str " f"but got '{type(primitive_type).__name__}'" ) lname = name.lower() if self._checking_enabled: if lname in self._data_symbols: raise SymbolTableError( f"Symbol table already contains a symbol for" f" a variable with name '{name}'" ) if lname in self._modules: raise SymbolTableError( f"Symbol table already contains a use of a " f"module with name '{name}'" ) for mod_name, mod in self._modules.items(): if mod.symbol_names and lname in mod.symbol_names: raise SymbolTableError( f"Symbol table already contains a use of a symbol " f"named '{name}' from module '{mod_name}'" ) self._data_symbols[lname] = SymbolTable.Symbol(lname, primitive_type.lower())
[docs] def add_use_symbols(self, name, only_list=None, rename_list=None): """ Creates an entry in the table for the USE of a module with the supplied name. If no `only_list` is supplied then this USE represents a wildcard import of all public symbols in the named module. If the USE statement has an ONLY clause but without any named symbols then `only_list` should be an empty list. A USE can also have one or more rename entries *without* an only list. :param str name: the name of the module being imported via a USE. Not \ case sensitive. :param only_list: if there is an 'only:' clause on the USE statement \ then this contains a list of tuples, each holding the local name \ of the symbol and its name in the module from which it is \ imported. These names are case insensitive. :type only_list: Optional[List[Tuple[str, str | NoneType]]] :param rename_list: a list of symbols that are renamed from the scope \ being imported. Each entry is a tuple containing the name in the \ local scope and the corresponding name in the module from which it\ is imported. These names are case insensitive. :type rename_list: Optional[List[Tuple[str, str]]] """ use = ModuleUse(name, only_list, rename_list) if use.name in self._modules: # The same module can appear in more than one use statement # in Fortran. self._modules[use.name].update(use) else: self._modules[use.name] = use
[docs] def lookup(self, name): """ Lookup the symbol with the supplied name. :param str name: the name of the symbol to lookup (not case sensitive). :returns: the named symbol. :rtype: :py:class:`fparser.two.symbol_table.SymbolTable.Symbol` :raises KeyError: if the named symbol cannot be found in this or any \ parent scope. """ # 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] 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}'")
@property
[docs] def name(self): """ :returns: the name of this symbol table (scoping region). :rtype: str """ return self._name
@property
[docs] def parent(self): """ :returns: the parent symbol table (scoping region) that contains \ this one (if any). :rtype: :py:class:`fparser.two.symbol_table.SymbolTable` or NoneType """ return self._parent
@parent.setter def parent(self, value): """ Set the parent scope for this symbol table. :param value: the parent symbol table. :type value: :py:class:`fparser.two.symbol_table.SymbolTable` :raises TypeError: if the supplied value is not None or a SymbolTable. """ if value is not None and not isinstance(value, SymbolTable): raise TypeError( f"Unless it is None, the parent of a SymbolTable must also be " f"a SymbolTable but got '{type(value).__name__}'" ) self._parent = value @property
[docs] 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
[docs] def add_child(self, child): """ Adds a child symbol table (scoping region nested within this one). :param child: the nested symbol table. :type child: :py:class:`fparser.two.symbol_table.SymbolTable` :raises TypeError: if the supplied child is not a SymbolTable. """ if not isinstance(child, SymbolTable): raise TypeError( f"Expected a SymbolTable instance but got '{type(child).__name__}'" ) self._children.append(child)
[docs] def del_child(self, name): """ Removes the named symbol table. :param str name: the name of the child symbol table to delete (not \ case sensitive). :raises KeyError: if the named table is not a child of this one. """ lname = name.lower() for child in self._children: if child.name == lname: self._children.remove(child) break else: raise KeyError( f"Symbol table '{self.name}' does not contain a table named '{name}'" )
@property
[docs] def children(self): """ :returns: the child (nested) symbol tables, if any. :rtype: list of :py:class:`fparser.two.symbol_table.SymbolTable` """ return self._children
@property
[docs] def root(self): """ :returns: the top-level symbol table that contains the current \ scoping region (symbol table). :rtype: :py:class:`fparser.two.symbol_table.SymbolTable` """ current = self while current.parent: current = current.parent return current
@property
[docs] 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
[docs] 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.
[docs] SYMBOL_TABLES = SymbolTables()
__all__ = ["SymbolTableError", "SymbolTables", "SymbolTable", "SYMBOL_TABLES"]