Repository: pyccel/pyccel (python)

Issue:
Tuple incorrectly identified as inhomogeneous
### Describe the bug
Tuples are occasionally misidentified as being inhomogeneous. This prevents them from being indexed.

### To Reproduce
Provide code to reproduce the behavior:
```python
def homogeneous_tuple_containing_var():
    elem = 4
    ai = (1, elem, 5)
    i = 1
    return ai[0], ai[i], ai[2]
```

### Error details
Provide the generated code, or the error message:
```
ERROR at annotation (semantic) stage
pyccel:
 |fatal [semantic]: tmp_idx.py [5,18]| Inhomogeneous tuples must be indexed with constant integers for the type inference to work (ai)
```

### Additional context
Found while cleaning up tuples in #1838 


Pull request:
Clean infer_type function
The function `infer_type` is critical to the `SemanticParser` but it has several issues which are fixed in this PR. Firstly the dictionary that is returned is modified to ensure that all the documented fields are defined. Fixes #1842 . This removes the need for calls to `dict.get` elsewhere in the code. Secondly the `is_target` property is removed. This property is either set to False or is incorrectly set. It is not used for calculations elsewhere.

A test is added for #1842. Further the tuple tests are updated so that all objects that are supposed to be homogeneous are indexed by a variable (this is not possible for inhomogeneous objects).

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28151b2d..40022055 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@ All notable changes to this project will be documented in this file.
 -   #1218 : Fix bug when assigning an array to a slice in Fortran.
 -   #1830 : Fix missing allocation when returning an annotated array expression.
 -   #1821 : Ensure an error is raised when creating an ambiguous interface.
+-   #1842 : Fix homogeneous tuples incorrectly identified as inhomogeneous.
 
 ### Changed
 -   #1720 : functions with the `@inline` decorator are no longer exposed to Python in the shared library.
@@ -53,6 +54,7 @@ All notable changes to this project will be documented in this file.
 -   \[INTERNALS\] Stop storing `FunctionDef`, `ClassDef`, and `Import` objects inside `CodeBlock`s.
 -   \[INTERNALS\] Remove the `order` argument from the `pyccel.ast.core.Allocate` constructor.
 -   \[INTERNALS\] Remove `rank` and `order` arguments from `pyccel.ast.variable.Variable` constructor.
+-   \[INTERNALS\] Ensure `SemanticParser.infer_type` returns all documented information.
 
 ### Deprecated
 
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index 0dcb5bb9..ffe706b4 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -505,7 +505,7 @@ class PythonTuple(TypedAstNode):
         orders = set(a.order for a in args)
         if len(ranks) == 1:
             rank = next(iter(ranks))
-            shapes = tuple(set(a.shape[i] for a in args if not (isinstance(a.shape[i], PyccelArrayShapeElement) or \
+            shapes = tuple(set(a.shape[i] for a in args if not (a.shape[i] is None or isinstance(a.shape[i], PyccelArrayShapeElement) or \
                                a.shape[i].get_attribute_nodes(PyccelArrayShapeElement))) \
                                for i in range(rank))
         else:
diff --git a/pyccel/ast/literals.py b/pyccel/ast/literals.py
index dd9f6625..ece541f8 100644
--- a/pyccel/ast/literals.py
+++ b/pyccel/ast/literals.py
@@ -332,6 +332,7 @@ class LiteralString(Literal):
     __slots__ = ('_string',)
     _class_type = StringType()
     _static_type = StringType()
+    _shape = (None,)
 
     def __init__(self, arg):
         super().__init__()
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index de71c028..8b8d79b8 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -669,9 +669,8 @@ class SemanticParser(BasicParser):
         inferred about the expression `expr`. This includes information about:
         - `class_type`
         - `shape`
-        - `memory_handling`
         - `cls_base`
-        - `is_target`
+        - `memory_handling`
 
         Parameters
         ----------
@@ -685,28 +684,19 @@ class SemanticParser(BasicParser):
             Dictionary containing all the type information which was inferred.
         """
         d_var = {
-                'shape'    : expr.shape,
                 'class_type' : expr.class_type,
+                'shape'      : expr.shape,
+                'cls_base'   : self.scope.find(str(expr.class_type), 'classes') or get_cls_base(expr.class_type),
+                'memory_handling' : 'heap' if expr.rank > 0 else 'stack'
             }
 
         if isinstance(expr, Variable):
             d_var['memory_handling'] = expr.memory_handling
-            d_var['class_type'     ] = expr.class_type
-            d_var['cls_base'       ] = expr.cls_base or self.scope.find(str(expr.dtype), 'classes')
-            d_var['is_target'      ] = expr.is_target
-            return d_var
-
-        elif isinstance(expr, LiteralString):
-            d_var['memory_handling'] = 'stack'
-            return d_var
-
-        elif isinstance(expr, PythonTuple):
-            d_var['cls_base'       ] = TupleClass
-            d_var['memory_handling'] = 'heap'
+            if expr.cls_base:
+                d_var['cls_base'   ] = expr.cls_base
             return d_var
 
         elif isinstance(expr, Concatenate):
-            d_var['cls_base'      ] = TupleClass
             if any(getattr(a, 'on_heap', False) for a in expr.args):
                 d_var['memory_handling'] = 'heap'
             else:
@@ -715,24 +705,16 @@ class SemanticParser(BasicParser):
 
         elif isinstance(expr, Duplicate):
             d = self._infer_type(expr.val)
-            d_var['cls_base'      ] = TupleClass
             if d.get('on_stack', False) and isinstance(expr.length, LiteralInteger):
                 d_var['memory_handling'] = 'stack'
             else:
                 d_var['memory_handling'] = 'heap'
             return d_var
 
-        elif isinstance(expr, NumpyNewArray):
-            d_var['cls_base'   ] = NumpyArrayClass
-            d_var['memory_handling'] = 'heap' if expr.rank > 0 else 'stack'
-            return d_var
-
         elif isinstance(expr, NumpyTranspose):
 
             var = expr.internal_var
 
-            d_var['cls_base'      ] = var.cls_base
-            d_var['is_target'     ] = var.is_target
             d_var['memory_handling'] = 'alias' if isinstance(var, Variable) else 'heap'
             return d_var
 
@@ -2992,7 +2974,6 @@ class SemanticParser(BasicParser):
             d_var = {'class_type' : dtype,
                     'memory_handling':'stack',
                     'shape' : None,
-                    'is_target' : False,
                     'cls_base' : cls_def,
                     }
             new_expression = []
@@ -3207,18 +3188,16 @@ class SemanticParser(BasicParser):
                 if name.startswith('Pyccel'):
                     name = name[6:]
                     d['cls_base'] = self.scope.find(name, 'classes')
-                    #TODO: Avoid writing the default variables here
-                    if d_var.get('is_target', False) or d_var.get('memory_handling', False) == 'alias':
+                    if d_var['memory_handling'] == 'alias':
                         d['memory_handling'] = 'alias'
                     else:
-                        d['memory_handling'] = d_var.get('memory_handling', False) or 'heap'
+                        d['memory_handling'] = d_var['memory_handling'] or 'heap'
 
                     # TODO if we want to use pointers then we set target to true
                     # in the ConsturcterCall
 
                 if isinstance(rhs, Variable) and rhs.is_target:
                     # case of rhs is a target variable the lhs must be a pointer
-                    d['is_target' ] = False
                     d['memory_handling'] = 'alias'
 
         lhs = expr.lhs


Test patch:
diff --git a/tests/epyccel/modules/tuples.py b/tests/epyccel/modules/tuples.py
index 11d90aea..77c01c67 100644
--- a/tests/epyccel/modules/tuples.py
+++ b/tests/epyccel/modules/tuples.py
@@ -2,17 +2,18 @@
 from pyccel.decorators import pure
 
 __all__ = [
-        'homogenous_tuple_int',
-        'homogenous_tuple_bool',
-        'homogenous_tuple_float',
-        'homogenous_tuple_string',
-        'homogenous_tuple_math',
+        'homogeneous_tuple_int',
+        'homogeneous_tuple_bool',
+        'homogeneous_tuple_float',
+        'homogeneous_tuple_string',
+        'homogeneous_tuple_math',
+        'homogeneous_tuple_containing_var',
         'homogeneous_tuple_of_arrays',
-        'inhomogenous_tuple_1',
-        'inhomogenous_tuple_2',
-        'inhomogenous_tuple_3',
-        'inhomogenous_tuple_2_levels_1',
-        'inhomogenous_tuple_2_levels_2',
+        'inhomogeneous_tuple_1',
+        'inhomogeneous_tuple_2',
+        'inhomogeneous_tuple_3',
+        'inhomogeneous_tuple_2_levels_1',
+        'inhomogeneous_tuple_2_levels_2',
         'homogeneous_tuple_2_levels',
         'tuple_unpacking_1',
         'tuple_unpacking_2',
@@ -76,45 +77,56 @@ __all__ = [
         'tuple_different_ranks',
         ]
 
-def homogenous_tuple_int():
+def homogeneous_tuple_int():
     ai = (1,4,5)
-    return ai[0], ai[1], ai[2]
+    i = 1
+    return ai[0], ai[i], ai[2]
 
-def homogenous_tuple_bool():
+def homogeneous_tuple_bool():
     ai = (False, True)
-    return ai[0], ai[1]
+    i = 1
+    return ai[0], ai[i]
 
-def homogenous_tuple_float():
+def homogeneous_tuple_float():
     ai = (1.5, 4.3, 5.2, 7.2, 9.999)
-    return ai[0], ai[1], ai[2], ai[3], ai[4]
+    i = 1
+    return ai[0], ai[i], ai[2], ai[3], ai[4]
 
-def homogenous_tuple_string():
+def homogeneous_tuple_string():
     ai = ('hello', 'tuple', 'world', '!!')
-    return ai[0], ai[1], ai[2], ai[3]
+    i = 1
+    return ai[0], ai[i], ai[2], ai[3]
 
-def homogenous_tuple_math():
+def homogeneous_tuple_math():
     ai = (4+5,3*9, 2**3)
-    return ai[0], ai[1], ai[2]
+    i = 1
+    return ai[0], ai[i], ai[2]
 
-def inhomogenous_tuple_1():
+def homogeneous_tuple_containing_var():
+    elem = 4
+    ai = (1, elem, 5)
+    i = 1
+    return ai[0], ai[i], ai[2]
+
+def inhomogeneous_tuple_1():
     ai = (0, False, 3+1j)
     return ai[0], ai[1], ai[2]
 
-def inhomogenous_tuple_2():
+def inhomogeneous_tuple_2():
     ai = (0, False, 3)
     return ai[0], ai[1], ai[2]
 
-def inhomogenous_tuple_3():
+def inhomogeneous_tuple_3():
     ai = (0, 1.0, 3)
     return ai[0], ai[1], ai[2]
 
-def inhomogenous_tuple_2_levels_1():
+def inhomogeneous_tuple_2_levels_1():
     # TODO [EB 15.06.21] Put back original test when strings are supported in C
     #ai = ((1,2), (4,False), (3.0, 'boo'))
     ai = ((1,2), (4,False), (3.0, True))
     return ai[0][0], ai[0][1], ai[1][0], ai[1][1], ai[2][0]
 
-def inhomogenous_tuple_2_levels_2():
+def inhomogeneous_tuple_2_levels_2():
     ai = ((0,1,2), (True,False,True))
     return ai[0][0], ai[0][1] ,ai[0][2], ai[1][0], ai[1][1], ai[1][2]
 
@@ -270,23 +282,27 @@ def tuples_inhomogeneous_copies_have_pointers():
 def tuples_mul_homogeneous():
     a = (1,2,3)
     b = a*2
-    return b[0], b[1], b[2], b[3], b[4], b[5]
+    i = 1
+    return b[0], b[i], b[2], b[3], b[4], b[5]
 
 def tuples_mul_homogeneous2():
     a = (1,2,3)
     b = 2*a
-    return b[0], b[1], b[2], b[3], b[4], b[5]
+    i = 1
+    return b[0], b[i], b[2], b[3], b[4], b[5]
 
 def tuples_mul_homogeneous3():
     a = (1,2,3)
     s = 2
     b = a*s
-    return b[0], b[1], b[2], b[3], b[4], b[5]
+    i = 1
+    return b[0], b[i], b[2], b[3], b[4], b[5]
 
 def tuples_mul_homogeneous4():
     s = 2
     b = (1,2,3)*s
-    return b[0], b[1], b[2], b[3], b[4], b[5]
+    i = 1
+    return b[0], b[i], b[2], b[3], b[4], b[5]
 
 def tuples_mul_inhomogeneous():
     a = (1,False)
@@ -318,23 +334,27 @@ def tuples_mul_inhomogeneous_2d():
 
 def tuples_add_homogeneous():
     a = (1,2,3) + (4,5,6)
-    return a[0], a[1], a[2], a[3], a[4], a[5]
+    i = 1
+    return a[0], a[i], a[2], a[3], a[4], a[5]
 
 def tuples_add_homogeneous_variables():
     a = (1,2,3)
     b = (4,5,6)
     c = a + b
-    return c[0], c[1], c[2], c[3], c[4], c[5]
+    i = 1
+    return c[0], c[i], c[2], c[3], c[4], c[5]
 
 def tuples_add_homogeneous_with_variables():
     a = (1,2,3)
     c = a + (4,5,6)
-    return c[0], c[1], c[2], c[3], c[4], c[5]
+    i = 1
+    return c[0], c[i], c[2], c[3], c[4], c[5]
 
 def tuples_add_homogeneous_with_variables2():
     a = (1,2,3)
     c = (4,5,6) + a
-    return c[0], c[1], c[2], c[3], c[4], c[5]
+    i = 1
+    return c[0], c[i], c[2], c[3], c[4], c[5]
 
 def tuples_add_inhomogeneous():
     a = (1,2,True) + (False,5,6)
@@ -374,7 +394,8 @@ def tuples_add_mixed_homogeneous_with_variables():
 def tuples_2d_sum():
     a = ((1,2), (3,4))
     b = a + ((5,6),)
-    return b[0][0], b[0][1], b[1][0], b[1][1], b[2][0], b[2][1]
+    i = 1
+    return b[0][0], b[0][i], b[1][0], b[i][i], b[2][0], b[2][1]
 
 def tuples_func():
     def my_tup():
@@ -410,23 +431,28 @@ def tuple_index():
 
 def tuple_homogeneous_int():
     a = tuple((1, 2, 3))
-    return a[0], a[1], a[2], len(a)
+    i = 1
+    return a[0], a[i], a[2], len(a)
 
 def tuple_homogeneous_bool():
     a = tuple((False, True))
-    return a[0], a[1], len(a)
+    i = 1
+    return a[0], a[i], len(a)
 
 def tuple_homogeneous_float():
     a = tuple((1.5, 4.3, 5.2, 7.2, 9.999))
-    return a[0], a[1], a[2], a[3], a[4], len(a)
+    i = 1
+    return a[0], a[i], a[2], a[3], a[4], len(a)
 
 def tuple_homogeneous_string():
     a = tuple(('hello', 'tuple', 'world', '!!'))
-    return a[0], a[1], a[2], a[3], len(a)
+    i = 1
+    return a[0], a[i], a[2], a[3], len(a)
 
 def tuple_homogeneous_math():
     a = tuple((4 + 5, 3 * 9, 2 ** 3))
-    return a[0], a[1], a[2], len(a)
+    i = 1
+    return a[0], a[i], a[2], len(a)
 
 def tuple_inhomogeneous_1():
     a = tuple((0, False, 3 + 1j))
diff --git a/tests/epyccel/test_tuples.py b/tests/epyccel/test_tuples.py
index 1ecec77c..9f21bb2a 100644
--- a/tests/epyccel/test_tuples.py
+++ b/tests/epyccel/test_tuples.py
@@ -17,7 +17,7 @@ tuple_funcs = [(f, getattr(tuples_module,f)) for f in tuples_module.__all__
                                             if is_func_with_0_args(f)]
 
 failing_tests = {
-        'homogenous_tuple_string':'String has no precision',
+        'homogeneous_tuple_string':"Can't save a list of strings (#459)",
         'tuple_homogeneous_return':"Can't return a tuple",
         'tuple_inhomogeneous_return':"Can't return a tuple",
         'tuple_visitation_inhomogeneous':"Can't iterate over an inhomogeneous tuple",