fparser Reference Guide  0.0.14
symbol_table.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2021-2022, Science and Technology Facilities Council.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are met:
9 #
10 # * Redistributions of source code must retain the above copyright notice, this
11 # list of conditions and the following disclaimer.
12 #
13 # * Redistributions in binary form must reproduce the above copyright notice,
14 # this list of conditions and the following disclaimer in the documentation
15 # and/or other materials provided with the distribution.
16 #
17 # * Neither the name of the copyright holder nor the names of its
18 # contributors may be used to endorse or promote products derived from
19 # this software without specific prior written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 # POSSIBILITY OF SUCH DAMAGE.
33 # -----------------------------------------------------------------------------
34 
35 """
36 The fparser2 symbol-table module. Defines various classes as well as
37 the single, global SYMBOL_TABLES instance. The latter is a container
38 for all of the top-level scoping units encountered during parsing.
39 
40 """
41 from collections import namedtuple
42 
43 
44 class SymbolTableError(Exception):
45  """Base class exception for symbol-table related errors."""
46 
47 
49  """
50  Class encapsulating functionality for the global symbol-tables object.
51  This is a container for all symbol tables constructed while parsing
52  code. All names are converted to lower case (since Fortran is not
53  case sensitive).
54 
55  """
56 
57  def __init__(self):
58  self._symbol_tables = {}
59  # Those classes that correspond to a new scoping unit
60  self._scoping_unit_classes = []
61  # The symbol table of the current scope
62  self._current_scope = None
63  # Whether or not we enable consistency checks in the symbol tables
64  # that are created.
65  self._enable_checks = False
66 
67  def __str__(self):
68  result = (
69  f"SymbolTables: {len(self._symbol_tables)} tables\n"
70  "========================\n"
71  )
72  return result + "\n".join(sorted(self._symbol_tables.keys()))
73 
74  def enable_checks(self, value):
75  """
76  Sets whether or not to enable consistency checks in every symbol
77  table that is created during a parse.
78 
79  :param bool value: whether or not checks are enabled.
80 
81  """
82  self._enable_checks = value
83 
84  def clear(self):
85  """
86  Deletes any stored SymbolTables but retains the stored list of
87  classes that define scoping units.
88 
89  """
90  self._symbol_tables = {}
91  self._current_scope = None
92 
93  def add(self, name):
94  """
95  Add a new symbol table with the supplied name. The name will be
96  converted to lower case if necessary.
97 
98  :param str name: the name for the new table.
99 
100  :returns: the new symbol table.
101  :rtype: :py:class:`fparser.two.symbol_table.SymbolTable`
102 
103  :raises SymbolTableError: if there is already an entry with the \
104  supplied name.
105  """
106  lower_name = name.lower()
107  if lower_name in self._symbol_tables:
108  raise SymbolTableError(
109  f"The table of top-level (un-nested) symbol tables already "
110  f"contains an entry for '{lower_name}'"
111  )
112  table = SymbolTable(lower_name, checking_enabled=self._enable_checks)
113  self._symbol_tables[lower_name] = table
114  return table
115 
116  def lookup(self, name):
117  """
118  Find the named symbol table and return it.
119 
120  :param str name: the name of the required symbol table (not case \
121  sensitive).
122 
123  :returns: the named symbol table.
124  :rtype: :py:class:`fparser.two.symbol_table.SymbolTable`
125 
126  """
127  return self._symbol_tables[name.lower()]
128 
129  @property
131  """
132  :returns: the fparser2 classes that are taken to mark the start of \
133  a new scoping region.
134  :rtype: list of types
135 
136  """
137  return self._scoping_unit_classes
138 
139  @scoping_unit_classes.setter
140  def scoping_unit_classes(self, value):
141  """
142  Set the list of fparser2 classes that are taken to mark the start of \
143  a new scoping region.
144 
145  :param value: the list of fparser2 classes.
146  :type value: list of types
147 
148  :raises TypeError: if the supplied value is not a list of types.
149 
150  """
151  if not isinstance(value, list):
152  raise TypeError(
153  f"Supplied value must be a list but got '{type(value).__name__}'"
154  )
155  if not all(isinstance(item, type) for item in value):
156  raise TypeError(f"Supplied list must contain only classes but got: {value}")
157  self._scoping_unit_classes = value
158 
159  @property
160  def current_scope(self):
161  """
162  :returns: the symbol table for the current scoping unit or None.
163  :rtype: :py:class:`fparser.two.symbol_table.SymbolTable` or NoneType
164  """
165  return self._current_scope
166 
167  def enter_scope(self, name):
168  """
169  Called when the parser enters a new scoping region (i.e. when it
170  encounters one of the classes listed in `_scoping_unit_classes`).
171  Sets the 'current scope' to be the symbol table with the supplied name.
172  If we are not currently within a tree of scoping regions then a
173  new entry is created in the internal dict of symbol tables. If there
174  is an existing tree then a new table is created and added to the
175  bottom.
176 
177  :param str name: name of the scoping region.
178 
179  """
180  lname = name.lower()
181 
182  if not self._current_scope:
183  # We're not already inside a nested scope.
184  try:
185  table = self.lookup(lname)
186  except KeyError:
187  # Create a new, top-level symbol table with the supplied name.
188  table = self.add(lname)
189  else:
190  # We are already inside a scoping region so create a new table
191  # and setup its parent/child connections.
192  table = SymbolTable(
193  lname, parent=self._current_scope, checking_enabled=self._enable_checks
194  )
195  self._current_scope.add_child(table)
196 
197  # Finally, make this new table the current scope
198  self._current_scope = table
199 
200  def exit_scope(self):
201  """
202  Marks the end of the processing of the current scoping unit. Since
203  we are exiting the current scoping region, the new 'current scoping
204  region' will be its parent.
205 
206  :raises SymbolTableError: if there is no current scope from which to \
207  exit.
208  """
209  if not self._current_scope:
210  raise SymbolTableError("exit_scope() called but no current scope exists.")
211  self._current_scope = self._current_scope.parent
212 
213  def remove(self, name):
214  """
215  Removes the named symbol table and any descendants it may have.
216  When searching for the named table, the current scope takes priority
217  followed by the list of top-level symbol tables.
218 
219  :param str name: the name of the symbol table to remove (not case \
220  sensitive).
221  :raises SymbolTableError: if the named symbol table is not in the \
222  current scope or in the list of top-level symbol tables.
223 
224  """
225  lname = name.lower()
226  if self._current_scope:
227  try:
228  self._current_scope.del_child(lname)
229  # We succeeded in removing it from the current scope so we
230  # are done.
231  return
232  except KeyError:
233  pass
234 
235  if lname not in self._symbol_tables:
236  msg = f"Failed to find a table named '{name}' in "
237  if self._current_scope:
238  msg += (
239  f"either the current scope (which contains "
240  f"{[child.name for child in self._current_scope.children]}) or "
241  )
242  msg += (
243  f"the list of top-level symbol tables "
244  f"({list(self._symbol_tables.keys())})."
245  )
246  raise SymbolTableError(msg)
247 
248  # Check that we're not currently somewhere inside the scope of the
249  # named table.
250  top_table = self._symbol_tables[lname]
251  if self._current_scope:
252  if self._current_scope.root is top_table:
253  raise SymbolTableError(
254  f"Cannot remove top-level symbol table '{name}' because the "
255  f"current scope '{self._current_scope.name}' has it as an ancestor."
256  )
257 
258  del self._symbol_tables[lname]
259 
260 
261 class ModuleUse:
262  """
263  Class capturing information on all USE statements referring to a given
264  Fortran module.
265 
266  A USE statement can rename an imported symbol so as to avoid a clash in
267  the local scope, e.g. `USE my_mod, alocal => amod` where `amod` is the
268  name of the symbol declared in `my_mod`. This renaming can also occur
269  inside an Only_List, e.g. `USE my_mod, only: alocal => amod`.
270 
271  :param str name: the name of the module.
272  :param only_list: list of 2-tuples giving the (local-name, module-name) \
273  of symbols that appear in an Only_List. If a symbol is not re-named \
274  then module-name can be None.
275  :type only_list: Optional[List[Tuple[str, str | NoneType]]]
276  :param rename_list: list of 2-tuples given the (local-name, module-name) \
277  of symbols that appear in a Rename_List.
278  :type rename_list: Optional[List[Tuple[str, str]]]
279 
280  :raises TypeError: if any of the supplied parameters are of the wrong type.
281 
282  """
283 
284  def __init__(self, name, only_list=None, rename_list=None):
285  if not isinstance(name, str):
286  raise TypeError(
287  f"The name of the module must be a str but got "
288  f"'{type(name).__name__}'"
289  )
290  self._validate_tuple_list("only", only_list)
291  self._validate_tuple_list("rename", rename_list)
292 
293  if only_list and not all(
294  isinstance(item[0], str) and (item[1] is None or isinstance(item[1], str))
295  for item in only_list
296  ):
297  raise TypeError(
298  f"If present, the only_list must be a list of "
299  f"2-tuples of (str, str | NoneType) but got: {only_list}"
300  )
301 
302  if rename_list and not all(
303  isinstance(item[0], str) and isinstance(item[1], str)
304  for item in rename_list
305  ):
306  raise TypeError(
307  f"If present, the rename_list must be a list of "
308  f"2-tuples of (str, str) but got: {rename_list}"
309  )
310 
311  self._name = name.lower()
312 
313  # dict of Symbols known to be accessed in this module.
314  self._symbols = {}
315  # Mapping from local symbol name in current scope to actual, declared
316  # name in the module from which it is imported.
317  self._local_to_module_map = {}
318 
319  if only_list is not None:
320  self._store_symbols(only_list)
321  self._wildcard_import = False
322  self._only_set = set(local_name.lower() for local_name, _ in only_list)
323  else:
324  self._only_set = None
325  self._wildcard_import = True
326 
327  if rename_list:
328  self._store_symbols(rename_list)
329  self._rename_set = set(local_name.lower() for local_name, _ in rename_list)
330  else:
331  self._rename_set = None
332 
333  @staticmethod
334  def _validate_tuple_list(name, tlist):
335  """
336  Validate the supplied list of tuples.
337 
338  :param str name: the name of the list being validated.
339  :param tlist: the list of tuples to validate.
340  :type tlist: Optional[List[Tuple[str, str | NoneType]]]
341 
342  :raises TypeError: if the supplied list is of the wrong type.
343 
344  """
345  if not tlist:
346  # None or an empty list is fine.
347  return
348 
349  if not isinstance(tlist, list):
350  raise TypeError(
351  f"If present, the {name}_list must be a list but "
352  f"got '{type(tlist).__name__}'"
353  )
354  if not all(isinstance(item, tuple) and len(item) == 2 for item in tlist):
355  raise TypeError(
356  f"If present, the {name}_list must be a list of "
357  f"2-tuples but got: {tlist}"
358  )
359 
360  def _store_symbols(self, rename_list):
361  """
362  Utility that updates the local list of symbols and renaming map for
363  the given list of local-name, module-name tuples. If a symbol is
364  not renamed then module-name can be None.
365 
366  :param rename_list: list of local-name, module-name pairs.
367  :type rename_list: List[Tuple[str, str | NoneType]]
368 
369  """
370  for local_name, orig_name in rename_list:
371  lname = local_name.lower()
372  oname = orig_name.lower() if orig_name else None
373  if lname not in self._symbols:
374  self._symbols[lname] = SymbolTable.Symbol(lname, "unknown")
375  if oname:
376  self._local_to_module_map[lname] = oname
377 
378  def update(self, other):
379  """
380  Update the current information with the information on the USE held
381  in 'other'.
382 
383  :param other: the other Use instance from which to update this one.
384  :type other: :py:class:`fparser.two.symbol_table.Use`
385 
386  :raises TypeError: if 'other' is of the wrong type.
387  :raises ValueError: if 'other' is for a module with a different \
388  name to the current one.
389 
390  """
391  if not isinstance(other, ModuleUse):
392  raise TypeError(
393  f"update() must be supplied with an instance of "
394  f"ModuleUse but got '{type(other).__name__}'"
395  )
396 
397  if self.name != other.name:
398  raise ValueError(
399  f"The ModuleUse supplied to update() is for module "
400  f"'{other.name}' but this ModuleUse is for module "
401  f"'{self.name}'"
402  )
403 
404  # Take the union of the only and rename lists and update the list
405  # of symbols.
406  if other.only_list:
407  for local_name in other.only_list:
408  if local_name not in self._symbols:
409  self._symbols[local_name] = SymbolTable.Symbol(
410  local_name, "unknown"
411  )
412  # pylint: disable=protected-access
413  if self._only_set is None:
414  self._only_set = other._only_set
415  else:
416  self._only_set = self._only_set.union(other._only_set)
417  # pylint: enable=protected-access
418 
419  if other.rename_list:
420  for local_name in other.rename_list:
421  if local_name not in self._symbols:
422  self._symbols[local_name] = SymbolTable.Symbol(
423  local_name, "unknown"
424  )
425  # pylint: disable=protected-access
426  if self._rename_set is None:
427  self._rename_set = other._rename_set
428  else:
429  self._rename_set = self._rename_set.union(other._rename_set)
430  # pylint: enable=protected-access
431 
432  # pylint: disable=protected-access
433  self._local_to_module_map.update(other._local_to_module_map)
434  # pylint: enable=protected-access
435 
436  self._wildcard_import = self._wildcard_import or other.wildcard_import
437 
438  @property
439  def name(self):
440  """
441  :returns: the name of the Fortran module.
442  :rtype: str
443  """
444  return self._name
445 
446  @property
447  def symbol_names(self):
448  """
449  :returns: the names of all symbols associated with USE(s) of this \
450  module.
451  :rtype: List[str]
452  """
453  return list(self._symbols.keys())
454 
455  @property
456  def only_list(self):
457  """
458  :returns: the local names that appear in an Only_List or None if there \
459  is no such list.
460  :rtype: Optional[List[str]]
461  """
462  if self._only_set is None:
463  return None
464  return list(self._only_set)
465 
466  @property
467  def rename_list(self):
468  """
469  :returns: the local names that appear in a Rename_List or None if there \
470  is no such list.
471  :rtype: Optional[List[str]]
472  """
473  if self._rename_set is None:
474  return None
475  return list(self._rename_set)
476 
477  @property
478  def wildcard_import(self):
479  """
480  :returns: whether there is a wildcard import from this module.
481  :rtype: bool
482  """
483  return self._wildcard_import
484 
485  def get_declared_name(self, name):
486  """
487  :returns: the name of the supplied symbol as declared in the module.
488  :rtype: str
489  """
490  return self._local_to_module_map.get(name, name)
491 
492 
494  """
495  Class implementing a single symbol table.
496 
497  Since this functionality is not yet fully mature, checks that new symbols
498  don't clash with existing symbols are disabled by default.
499  Once #201 is complete it is planned to switch this so that the checks
500  are instead enabled by default.
501 
502  :param str name: the name of this scope. Will be the name of the \
503  associated module or routine.
504  :param parent: the symbol table within which this one is nested (if any).
505  :type parent: :py:class:`fparser.two.symbol_table.SymbolTable.Symbol`
506  :param bool checking_enabled: whether or not validity checks are \
507  performed for symbols added to the table.
508 
509  """
510 
511  # TODO #201 add support for other symbol properties (kind, shape
512  # and visibility). We may need a distinct Symbol class so as to provide
513  # type checking for the various properties.
514  Symbol = namedtuple("Symbol", "name primitive_type")
515 
516  def __init__(self, name, parent=None, checking_enabled=False):
517  self._name = name.lower()
518  # Symbols defined in this scope that represent data.
519  self._data_symbols = {}
520  # dict of ModuleUse objects (indexed by module name) representing
521  # modules imported into this scope.
522  self._modules = {}
523  # Reference to a SymbolTable that contains this one (if any). Actual
524  # value (if any) is set via setter method.
525  self._parent = None
526  self.parent = parent
527  # Whether or not to perform validity checks when symbols are added.
528  self._checking_enabled = checking_enabled
529  # Symbol tables nested within this one.
530  self._children = []
531 
532  def __str__(self):
533  header = "===========\n"
534  symbols = "Symbols:\n"
535  if self._data_symbols:
536  symbols += "\n".join(list(self._data_symbols.keys())) + "\n"
537  uses = "Used modules:\n"
538  if self._modules:
539  uses += "\n".join(list(self._modules.keys())) + "\n"
540  return f"{header}Symbol Table '{self._name}'\n{symbols}{uses}{header}"
541 
542  def add_data_symbol(self, name, primitive_type):
543  """
544  Creates a new Symbol with the specified properties and adds it to
545  the symbol table. The supplied name is converted to lower case.
546 
547  TODO #201 add support for other symbol properties (kind, shape
548  and visibility).
549 
550  :param str name: the name of the symbol.
551  :param str primitive_type: the primitive type of the symbol.
552 
553  :raises TypeError: if any of the supplied parameters are of the \
554  wrong type.
555  :raises SymbolTableError: if the symbol table already contains an
556  entry with the supplied name.
557  """
558  if not isinstance(name, str):
559  raise TypeError(
560  f"The name of the symbol must be a str but got "
561  f"'{type(name).__name__}'"
562  )
563  # TODO #201 use an enumeration for the primitive type
564  if not isinstance(primitive_type, str):
565  raise TypeError(
566  f"The primitive type of the symbol must be specified as a str "
567  f"but got '{type(primitive_type).__name__}'"
568  )
569  lname = name.lower()
570 
571  if self._checking_enabled:
572  if lname in self._data_symbols:
573  raise SymbolTableError(
574  f"Symbol table already contains a symbol for"
575  f" a variable with name '{name}'"
576  )
577  if lname in self._modules:
578  raise SymbolTableError(
579  f"Symbol table already contains a use of a "
580  f"module with name '{name}'"
581  )
582  for mod_name, mod in self._modules.items():
583  if mod.symbol_names and lname in mod.symbol_names:
584  raise SymbolTableError(
585  f"Symbol table already contains a use of a symbol "
586  f"named '{name}' from module '{mod_name}'"
587  )
588 
589  self._data_symbols[lname] = SymbolTable.Symbol(lname, primitive_type.lower())
590 
591  def add_use_symbols(self, name, only_list=None, rename_list=None):
592  """
593  Creates an entry in the table for the USE of a module with the supplied
594  name. If no `only_list` is supplied then this USE represents a wildcard
595  import of all public symbols in the named module. If the USE statement
596  has an ONLY clause but without any named symbols then `only_list`
597  should be an empty list.
598 
599  A USE can also have one or more rename entries *without* an only list.
600 
601  :param str name: the name of the module being imported via a USE. Not \
602  case sensitive.
603  :param only_list: if there is an 'only:' clause on the USE statement \
604  then this contains a list of tuples, each holding the local name \
605  of the symbol and its name in the module from which it is \
606  imported. These names are case insensitive.
607  :type only_list: Optional[List[Tuple[str, str | NoneType]]]
608  :param rename_list: a list of symbols that are renamed from the scope \
609  being imported. Each entry is a tuple containing the name in the \
610  local scope and the corresponding name in the module from which it\
611  is imported. These names are case insensitive.
612  :type rename_list: Optional[List[Tuple[str, str]]]
613 
614  """
615  use = ModuleUse(name, only_list, rename_list)
616 
617  if use.name in self._modules:
618  # The same module can appear in more than one use statement
619  # in Fortran.
620  self._modules[use.name].update(use)
621  else:
622  self._modules[use.name] = use
623 
624  def lookup(self, name):
625  """
626  Lookup the symbol with the supplied name.
627 
628  :param str name: the name of the symbol to lookup (not case sensitive).
629 
630  :returns: the named symbol.
631  :rtype: :py:class:`fparser.two.symbol_table.SymbolTable.Symbol`
632 
633  :raises KeyError: if the named symbol cannot be found in this or any \
634  parent scope.
635  """
636  # Fortran is not case sensitive so convert input to lowercase.
637  lname = name.lower()
638  if lname in self._data_symbols:
639  return self._data_symbols[lname]
640  # No match in this scope - search in parent scope (if any)
641  if self.parent:
642  return self.parent.lookup(lname)
643  raise KeyError(f"Failed to find symbol named '{lname}'")
644 
645  @property
646  def name(self):
647  """
648  :returns: the name of this symbol table (scoping region).
649  :rtype: str
650  """
651  return self._name
652 
653  @property
654  def parent(self):
655  """
656  :returns: the parent symbol table (scoping region) that contains \
657  this one (if any).
658  :rtype: :py:class:`fparser.two.symbol_table.SymbolTable` or NoneType
659  """
660  return self._parent
661 
662  @parent.setter
663  def parent(self, value):
664  """
665  Set the parent scope for this symbol table.
666 
667  :param value: the parent symbol table.
668  :type value: :py:class:`fparser.two.symbol_table.SymbolTable`
669 
670  :raises TypeError: if the supplied value is not None or a SymbolTable.
671 
672  """
673  if value is not None and not isinstance(value, SymbolTable):
674  raise TypeError(
675  f"Unless it is None, the parent of a SymbolTable must also be "
676  f"a SymbolTable but got '{type(value).__name__}'"
677  )
678  self._parent = value
679 
680  def add_child(self, child):
681  """
682  Adds a child symbol table (scoping region nested within this one).
683 
684  :param child: the nested symbol table.
685  :type child: :py:class:`fparser.two.symbol_table.SymbolTable`
686 
687  :raises TypeError: if the supplied child is not a SymbolTable.
688 
689  """
690  if not isinstance(child, SymbolTable):
691  raise TypeError(
692  f"Expected a SymbolTable instance but got '{type(child).__name__}'"
693  )
694  self._children.append(child)
695 
696  def del_child(self, name):
697  """
698  Removes the named symbol table.
699 
700  :param str name: the name of the child symbol table to delete (not \
701  case sensitive).
702 
703  :raises KeyError: if the named table is not a child of this one.
704 
705  """
706  lname = name.lower()
707  for child in self._children:
708  if child.name == lname:
709  self._children.remove(child)
710  break
711  else:
712  raise KeyError(
713  f"Symbol table '{self.name}' does not contain a table named '{name}'"
714  )
715 
716  @property
717  def children(self):
718  """
719  :returns: the child (nested) symbol tables, if any.
720  :rtype: list of :py:class:`fparser.two.symbol_table.SymbolTable`
721  """
722  return self._children
723 
724  @property
725  def root(self):
726  """
727  :returns: the top-level symbol table that contains the current \
728  scoping region (symbol table).
729  :rtype: :py:class:`fparser.two.symbol_table.SymbolTable`
730 
731  """
732  current = self
733  while current.parent:
734  current = current.parent
735  return current
736 
737 
738 #: The single, global container for all symbol tables constructed while
739 #: parsing.
740 SYMBOL_TABLES = SymbolTables()
741 
742 
743 __all__ = ["SymbolTableError", "SymbolTables", "SymbolTable", "SYMBOL_TABLES"]
def add_data_symbol(self, name, primitive_type)
def add_use_symbols(self, name, only_list=None, rename_list=None)
def _store_symbols(self, rename_list)