Repository: stfc/PSyclone

Issue:
only_exact_name parameter for find_or_create_tag on SymbolTable
When creating variables which need a tag (to find later) and we need to guarantee that the created symbol will have the correct name (e.g. if they are an import from a module) an `only_exact_name` parameter would be helpful.

If this cannot be met I think we just throw an exception and fail for now.

Discussion:

Patch:
diff --git a/changelog b/changelog
index 72c906cf0..66c8e2a48 100644
--- a/changelog
+++ b/changelog
@@ -33,6 +33,9 @@
 
 	13) PR #2539 for #2538. Add PSyIR Symbol PreprocessorInterface.
 
+	14) PR #2540 for #2537. Add 'allow_renaming' parameter to the symbol_table
+	new_symbol method and improve its import handling.
+
 release 2.5.0 14th of February 2024
 
 	1) PR #2199 for #2189. Fix bugs with missing maps in enter data
diff --git a/src/psyclone/psyir/symbols/symbol_table.py b/src/psyclone/psyir/symbols/symbol_table.py
index cb7f3fd6e..3acc07a21 100644
--- a/src/psyclone/psyir/symbols/symbol_table.py
+++ b/src/psyclone/psyir/symbols/symbol_table.py
@@ -340,7 +340,7 @@ class SymbolTable():
         return cls._normalize(string1) == cls._normalize(string2)
 
     def new_symbol(self, root_name=None, tag=None, shadowing=False,
-                   symbol_type=None, **symbol_init_args):
+                   symbol_type=None, allow_renaming=True, **symbol_init_args):
         ''' Create a new symbol. Optional root_name and shadowing
         arguments can be given to choose the name following the rules of
         next_available_name(). An optional tag can also be given.
@@ -348,22 +348,29 @@ class SymbolTable():
         and any additional initialization keyword arguments of this
         symbol_type can be provided to refine the created Symbol.
 
-        :param root_name: optional name to use when creating a new \
-            symbol name. This will be appended with an integer if the name \
-            clashes with an existing symbol name.
-        :type root_name: str or NoneType
+        :param root_name: optional name to use when creating a
+                          new symbol name. This will be appended
+                          with an integer if the name
+                          clashes with an existing symbol name.
+        :type root_name: Optional[str]
         :param str tag: optional tag identifier for the new symbol.
-        :param bool shadowing: optional logical flag indicating whether the \
-            name can be overlapping with a symbol in any of the ancestors \
+        :param bool shadowing: optional logical flag indicating whether the
+            name can be overlapping with a symbol in any of the ancestors
             symbol tables. Defaults to False.
         :param symbol_type: class type of the new symbol.
-        :type symbol_type: type object of class (or subclasses) of \
+        :type symbol_type: type object of class (or subclasses) of
                            :py:class:`psyclone.psyir.symbols.Symbol`
+        :param bool allow_renaming: whether to allow the newly created
+                                    symbol to be renamed from root_name.
+                                    Defaults to True.
         :param symbol_init_args: arguments to create a new symbol.
-        :type symbol_init_args: unwrapped Dict[str] = object
+        :type symbol_init_args: unwrapped dict[str, Any]
 
-        :raises TypeError: if the type_symbol argument is not the type of a \
+        :raises TypeError: if the type_symbol argument is not the type of a
                            Symbol object class or one of its subclasses.
+        :raises SymbolError: if the the symbol needs to be created but would
+                             need to be renamed to be created and
+                             allow_renaming is False.
 
         '''
         # Only type-check symbol_type, the other arguments are just passed down
@@ -384,6 +391,20 @@ class SymbolTable():
             symbol_init_args["visibility"] = self.default_visibility
 
         available_name = self.next_available_name(root_name, shadowing)
+        # TODO #2546 - we should disallow renaming of UnsupportedFortranTypes
+        if (not allow_renaming and available_name != root_name):
+            raise SymbolError(
+                f"Cannot create symbol '{root_name}' as a symbol with that "
+                f"name already exists in this scope, and renaming is "
+                f"disallowed."
+            )
+        if "interface" in symbol_init_args and available_name != root_name:
+            interface = symbol_init_args["interface"]
+            if isinstance(interface, ImportInterface):
+                symbol_init_args["interface"] = ImportInterface(
+                    symbol_init_args["interface"].container_symbol,
+                    root_name
+                )
         symbol = symbol_type(available_name, **symbol_init_args)
         self.add(symbol, tag)
         return symbol
@@ -417,7 +438,8 @@ class SymbolTable():
         except KeyError:
             return self.new_symbol(name, **new_symbol_args)
 
-    def find_or_create_tag(self, tag, root_name=None, **new_symbol_args):
+    def find_or_create_tag(self, tag, root_name=None,
+                           **new_symbol_args):
         ''' Lookup a tag, if it doesn't exist create a new symbol with the
         given tag. By default it creates a generic Symbol with the tag as the
         root of the symbol name. Optionally, a different root_name or any of
@@ -427,8 +449,18 @@ class SymbolTable():
         :param str tag: tag identifier.
         :param str root_name: optional name of the new symbol if it needs \
                               to be created. Otherwise it is ignored.
+        :param bool shadowing: optional logical flag indicating whether the
+            name can be overlapping with a symbol in any of the ancestors
+            symbol tables. Defaults to False.
+        :param symbol_type: class type of the new symbol.
+        :type symbol_type: type object of class (or subclasses) of
+                           :py:class:`psyclone.psyir.symbols.Symbol`
+        :param bool allow_renaming: whether to allow the newly created
+        :param bool allow_renaming: whether to allow the newly created
+                                    symbol to be renamed from root_name.
+                                    Defaults to True.
         :param new_symbol_args: arguments to create a new symbol.
-        :type new_symbol_args: unwrapped Dict[str, object]
+        :type new_symbol_args: unwrapped dict[str, Any]
 
         :returns: symbol associated with the given tag.
         :rtype: :py:class:`psyclone.psyir.symbols.Symbol`


Test patch:
diff --git a/src/psyclone/tests/psyir/symbols/symbol_table_test.py b/src/psyclone/tests/psyir/symbols/symbol_table_test.py
index 995371e00..930cd5c3b 100644
--- a/src/psyclone/tests/psyir/symbols/symbol_table_test.py
+++ b/src/psyclone/tests/psyir/symbols/symbol_table_test.py
@@ -266,6 +266,52 @@ def test_new_symbol_5():
                 == "symbol1")
 
 
+def test_new_symbol_import_interface():
+    '''Check that if a symbol is renamed and is from an import it has
+     the correct original name.'''
+    sym_table = symbols.SymbolTable()
+    my_mod = symbols.ContainerSymbol("my_mod")
+    sym_table.add(my_mod)
+    sym_table.new_symbol("generic")
+    renamed = sym_table.new_symbol("generic",
+                                   interface=symbols.ImportInterface(my_mod))
+    assert renamed.name == "generic_1"
+    assert renamed.interface.orig_name == "generic"
+
+
+def test_new_symbol_fails_on_duplicate_without_renaming():
+    '''Check if a new symbol has the same name as an existing symbol, and
+    that symbol is not allowed to be renamed we get the appropriate exception.
+    '''
+    # Prepare the symbol table hierarchy for the test
+    schedule_symbol_table, _ = create_hierarchy()
+    symbol = schedule_symbol_table.lookup("symbol1")
+    symbol.constant_value = 3
+    symbol2 = schedule_symbol_table.lookup("symbol2")
+
+    # Create multiple references to the symbol
+    array_type = symbols.ArrayType(symbols.REAL_TYPE, [Reference(symbol)])
+    schedule_symbol_table.new_symbol("array",
+                                     symbol_type=symbols.DataSymbol,
+                                     datatype=array_type)
+    sched = schedule_symbol_table.node
+    ref1 = Reference(symbol2)
+    ref2 = Reference(symbol)
+    assignment = Assignment.create(ref1, ref2)
+    sched.addchild(assignment)
+
+    # Check we fail if we attempt to add another symbol with the same name
+    # and disallow renaming.
+    with pytest.raises(symbols.SymbolError) as err:
+        schedule_symbol_table.new_symbol("array",
+                                         symbol_type=symbols.DataSymbol,
+                                         datatype=array_type,
+                                         allow_renaming=False)
+    assert ("Cannot create symbol 'array' as a symbol with that name already "
+            "exists in this scope, and renaming is disallowed."
+            in str(err.value))
+
+
 def test_add_1():
     '''Test that the add method inserts new symbols in the symbol table,
     but raises appropriate errors when provided with an invalid symbol
@@ -2025,6 +2071,17 @@ def test_find_or_create_tag():
     assert ("Expected symbol with tag 'tag3' to be of type 'RoutineSymbol' "
             "but found type 'DataSymbol'." in str(err.value))
 
+    tag5 = symtab.find_or_create_tag("tag5", root_name="var",
+                                     allow_renaming=True)
+    assert tag5.name == "var_1"
+    # Check that it fails if exact_name was specified but we couldn't create
+    # a variable with that name.
+    with pytest.raises(symbols.SymbolError) as err:
+        symtab.find_or_create_tag("tag6", root_name="var",
+                                  allow_renaming=False)
+    assert ("Cannot create symbol 'var' as a symbol with that name already "
+            "exists in this scope, and renaming is disallowed."
+            in str(err.value))
     # TODO #1057: It should also fail the symbol is found but the properties
     # are different than the requested ones.