Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Closes #313) permit intrinsic shadowing #418

Merged
merged 23 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
248a618
#313 change Intrinsic match to return None if no. of args differs fro…
arporter Mar 7, 2022
8ad1a22
Merge branch 'master' into 313_permit_intrinsic_shadowing
arporter Jul 28, 2022
335fb07
Merge branch 'master' into 313_permit_intrinsic_shadowing
arporter Jun 12, 2023
999e499
Merge branch 'master' into 313_permit_intrinsic_shadowing
arporter Sep 14, 2023
f50ade6
#313 add wildcard_imports property to SymbolTable
arporter Sep 14, 2023
e8238e0
#313 update Dev Guide
arporter Sep 14, 2023
602b368
#313 check for wildcard imports before raising syntax error on intrin…
arporter Sep 14, 2023
44c3907
#313 add new tests for intrinsic
arporter Sep 14, 2023
22337ad
#313 extend test to get coverage
arporter Sep 14, 2023
207d3ba
#418 add comment
arporter Sep 27, 2023
1500119
#418 improve testing
arporter Sep 28, 2023
911b1ae
#313 further test improvements
arporter Sep 28, 2023
03dc8d1
Merge branch 'master' into 313_permit_intrinsic_shadowing
arporter Sep 28, 2023
6f15622
#313 add node and all_symbols_resolved properties to symbol table.
arporter Oct 20, 2023
6dc0dde
Merge branch 'master' into 313_permit_intrinsic_shadowing
arporter Oct 20, 2023
41f83a3
#313 improve testing of new methods
arporter Oct 20, 2023
b04834c
Merge branch '313_permit_intrinsic_shadowing' of github.com:stfc/fpar…
arporter Oct 20, 2023
9029e9b
#313 improve testing of wildcard_imports()
arporter Oct 20, 2023
45c9dab
#418 updates for review
arporter Dec 6, 2023
f791adf
#418 remove missed line since #170 is closed.
arporter Jan 4, 2024
31117b8
#418 update copyright date
arporter Jan 4, 2024
e4513e5
Merge remote-tracking branch 'origin/master' into 313_permit_intrinsi…
sergisiso Jan 25, 2024
f7ebb67
#418 Update changelog
sergisiso Jan 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/developers_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
143 changes: 79 additions & 64 deletions src/fparser/two/Fortran2003.py
Original file line number Diff line number Diff line change
Expand Up @@ -12333,77 +12333,92 @@

: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

:raises InternalSyntaxError: If the number of arguments \
provided does not match the number of arguments expected by \
the intrinsic.
: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 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

# 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

Check warning on line 12375 in src/fparser/two/Fortran2003.py

View check run for this annotation

Codecov / codecov/patch

src/fparser/two/Fortran2003.py#L12375

Added line #L12375 was not covered by tests

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


Expand Down
111 changes: 98 additions & 13 deletions src/fparser/two/symbol_table.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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

Expand All @@ -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`).
Expand All @@ -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()

Expand All @@ -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,
rupertford marked this conversation as resolved.
Show resolved Hide resolved
)
self._current_scope.add_child(table)

Expand Down Expand Up @@ -413,12 +419,22 @@ 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
rupertford marked this conversation as resolved.
Show resolved Hide resolved
this module.
"""
return self._symbols[name.lower()]

@property
def only_list(self):
"""
Expand Down Expand Up @@ -466,21 +482,24 @@ 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
# and visibility). We may need a distinct Symbol class so as to provide
# 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 = {}
Expand All @@ -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.
Expand Down Expand Up @@ -604,6 +633,11 @@ def lookup(self, name):
lname = name.lower()
if lname in self._data_symbols:
return self._data_symbols[lname]
for module in self._modules.values():
try:
return module.lookup(lname)
except KeyError:
pass
rupertford marked this conversation as resolved.
Show resolved Hide resolved
# No match in this scope - search in parent scope (if any)
if self.parent:
return self.parent.lookup(lname)
Expand Down Expand Up @@ -644,6 +678,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).
Expand Down Expand Up @@ -701,6 +743,49 @@ def root(self):
current = current.parent
return current

@property
rupertford marked this conversation as resolved.
Show resolved Hide resolved
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:
rupertford marked this conversation as resolved.
Show resolved Hide resolved
# 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.
rupertford marked this conversation as resolved.
Show resolved Hide resolved
: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.
Expand Down
Loading