Repository: pyccel/pyccel
Pull request #2258 (2025-03-24 17:17:00)

Add the necessary pointer information to ensure errors are raised for multi-level containers
Add the necessary pointer information in the semantic stage using `_indicate_pointer_target` to ensure errors are raised for multi-level containers (e.g. lists of lists) if a pointer is returned while its target goes out of scope. This is a first step towards #2210 .

pyccel-bot[bot]: Hello again! Thank you for this new pull request :star_struck:.

Please begin by requesting your checklist using the command `/bot checklist`

pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

EmilyBourne: /bot run coverage
pyccel-bot[bot]: Hey @pyccel/pyccel-dev ! @EmilyBourne has just created this great new pull request! Check it out and let me know what you think!

EmilyBourne: /bot run pr_tests

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 902ac54b..a23058e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -141,6 +141,7 @@ All notable changes to this project will be documented in this file.
 -   #2098 : Fix using multiple list comprehensions.
 -   #1948 : Fix list comprehension does not work in C.
 -   #2245 : Fix internal error when an inhomogeneous tuple appears as an indexed element.
+-   #2258 : Fix missing errors for bad pointer handling in the case of containers with mutable elements.
 
 ### Changed
 
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 768e9f75..54c694ab 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -819,6 +819,15 @@ class IndexedElement(TypedAstNode):
         """
         return self.base.allows_negative_indexes
 
+    @property
+    def is_slice(self):
+        """
+        Indicates whether this instance represents a slice.
+
+        Indicates whether this instance represents a slice or an element.
+        """
+        return self._is_slice
+
     def __hash__(self):
         return hash((self.base, self._indices))
 
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index d66d1c57..e02a354c 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -1237,7 +1237,10 @@ class PythonCodePrinter(CodePrinter):
         cls_variable = expr.cls_variable
         cls_name = cls_variable.cls_base.name
         args = ', '.join(self._print(arg) for arg in expr.args[1:])
-        return f"{cls_variable} = {cls_name}({args})\n"
+        if expr.get_direct_user_nodes(lambda u: isinstance(u, CodeBlock)):
+            return f"{cls_variable} = {cls_name}({args})\n"
+        else:
+            return f"{cls_name}({args})"
 
     def _print_Del(self, expr):
         return ''.join(f'del {var.variable}\n' for var in expr.variables)
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 3854bbe8..ab3d1e2a 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -36,7 +36,7 @@ from pyccel.ast.builtins import PythonRange, PythonZip, PythonEnumerate, PythonT
 from pyccel.ast.builtins import Lambda, PythonMap
 
 from pyccel.ast.builtin_methods.dict_methods import DictKeys
-from pyccel.ast.builtin_methods.list_methods import ListAppend, ListPop
+from pyccel.ast.builtin_methods.list_methods import ListAppend, ListPop, ListInsert
 from pyccel.ast.builtin_methods.set_methods  import SetAdd, SetUnion, SetCopy, SetIntersectionUpdate
 from pyccel.ast.builtin_methods.set_methods  import SetPop
 from pyccel.ast.builtin_methods.dict_methods  import DictGetItem, DictGet, DictPop, DictPopitem
@@ -698,9 +698,13 @@ class SemanticParser(BasicParser):
             return
 
         assert pointer != target
+        assert not isinstance(pointer.class_type, (StringType, FixedSizeNumericType))
+
+        # The class itself should also be aware of the target for freeing
         if isinstance(pointer, DottedVariable):
             self._indicate_pointer_target(pointer.lhs, target, expr)
-        elif isinstance(target, DottedVariable):
+
+        if isinstance(target, DottedVariable):
             self._indicate_pointer_target(pointer, target.lhs, expr)
         elif isinstance(target, IndexedElement):
             self._indicate_pointer_target(pointer, target.base, expr)
@@ -708,16 +712,37 @@ class SemanticParser(BasicParser):
             self._indicate_pointer_target(pointer, target.dict_obj, expr)
         elif isinstance(target, Variable):
             if target.is_alias:
+                sub_targets = None
                 try:
                     sub_targets = self._pointer_targets[-1][target]
                 except KeyError:
                     errors.report("Pointer cannot point at a non-local pointer\n"+PYCCEL_RESTRICTION_TODO,
                         severity='error', symbol=expr)
-                self._pointer_targets[-1].setdefault(pointer, []).extend((t[0], expr) for t in sub_targets)
+                if sub_targets:
+                    self._pointer_targets[-1].setdefault(pointer, []).extend((t[0], expr) for t in sub_targets)
             else:
                 target.is_target = True
                 self._pointer_targets[-1].setdefault(pointer, []).append((target, expr))
-        else:
+        elif isinstance(target, FunctionCall):
+            if isinstance(target.funcdef, FunctionDef):
+                if target.funcdef.result_pointer_map:
+                    raise NotImplementedError("TODO results point at args")
+        elif isinstance(target, (PythonList, PythonSet, PythonTuple)):
+            if not isinstance(target.class_type.element_type, (StringType, FixedSizeNumericType)):
+                for v in target:
+                    self._indicate_pointer_target(pointer, v, expr)
+        elif isinstance(target, (ListPop, DictPop)):
+            target_var = target.list_obj if isinstance(target, ListPop) else target.dict_obj
+            if target_var in self._pointer_targets[-1]:
+                sub_targets = self._pointer_targets[-1][target_var]
+                self._pointer_targets[-1].setdefault(pointer, []).extend((t[0], expr) for t in sub_targets)
+            elif isinstance(pointer, Variable):
+                self._allocs[-1].add(pointer)
+        elif isinstance(target, PythonDict):
+            if not isinstance(target.class_type.value_type, (StringType, FixedSizeNumericType)):
+                for v in target.values:
+                    self._indicate_pointer_target(pointer, v, expr)
+        elif isinstance(pointer, Variable) and pointer.is_alias:
             errors.report("Pointer cannot point at a temporary object",
                 severity='error', symbol=expr)
 
@@ -1166,9 +1191,10 @@ class SemanticParser(BasicParser):
                             new_expr = DottedName(args[0].value, FunctionCall(func, args[1:]))
                         else:
                             new_expr = FunctionCall(func, args)
-                        new_expr.set_current_ast(expr.python_ast)
+                        new_expr.set_current_ast(expr.python_ast or self._current_ast_node)
                         pyccel_stage.set_stage('semantic')
-                        new_expr.set_current_user_node(expr.current_user_node)
+                        for u in expr.get_all_user_nodes():
+                            new_expr.set_current_user_node(u)
                         expr = new_expr
                     return getattr(self, annotation_method)(expr, args)
 
@@ -1457,19 +1483,31 @@ class SemanticParser(BasicParser):
             Dictionary of properties for the new Variable.
         """
 
+        # rhs is None in an AugAssign
+        if rhs is None or isinstance(rhs, FunctionalFor):
+            return
+
+        assert rhs.pyccel_staging != 'syntactic'
+
         if isinstance(rhs, NumpyTranspose) and rhs.internal_var.on_heap:
             d_lhs['memory_handling'] = 'alias'
             rhs.internal_var.is_target = True
 
-        if isinstance(rhs, Variable) and (rhs.rank > 0 or isinstance(rhs.class_type, CustomDataType)) \
-                and not isinstance(rhs.class_type, (TupleType, StringType)):
-            d_lhs['memory_handling'] = 'alias'
-            rhs.is_target = not rhs.is_alias
+        if not isinstance(rhs.class_type, (TupleType, StringType, FixedSizeNumericType)):
+            if isinstance(rhs, Variable):
+                d_lhs['memory_handling'] = 'alias'
+                rhs.is_target = not rhs.is_alias
 
-        if isinstance(rhs, IndexedElement) and rhs.rank > 0 and \
-                (getattr(rhs.base, 'is_ndarray', False) or getattr(rhs.base, 'is_alias', False)):
-            d_lhs['memory_handling'] = 'alias'
-            rhs.base.is_target = not rhs.base.is_alias
+            elif isinstance(rhs, IndexedElement) and \
+                    isinstance(rhs.class_type, (HomogeneousTupleType, NumpyNDArrayType)):
+                d_lhs['memory_handling'] = 'alias'
+                rhs.base.is_target = not rhs.base.is_alias
+
+            elif isinstance(rhs, (ListPop, DictPop)):
+                d_lhs['memory_handling'] = 'alias'
+
+            elif isinstance(rhs, IndexedElement) and not rhs.is_slice:
+                d_lhs['memory_handling'] = 'alias'
 
     def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign = False,
             arr_in_multirets=False):
@@ -1882,7 +1920,7 @@ class SemanticParser(BasicParser):
 
                     if status == 'unallocated':
                         self._allocs[-1].add(var)
-                    else:
+                    elif isinstance(var.class_type, NumpyNDArrayType):
                         errors.report(ARRAY_REALLOCATION.format(class_type = var.class_type), symbol=var.name,
                             severity='warning',
                             bounding_box=(self.current_ast_node.lineno,
@@ -1926,7 +1964,7 @@ class SemanticParser(BasicParser):
         while isinstance(loop, (For, If)):
 
             nlevels+=1
-            self._get_for_iterators(loop.iterable, loop.target, new_expr)
+            self._get_for_iterators(loop.iterable, loop.target, new_expr, expr)
 
             loop_elem = loop.body.body[0]
             if isinstance(loop_elem, If):
@@ -2237,7 +2275,7 @@ class SemanticParser(BasicParser):
 
         return iterable
 
-    def _get_for_iterators(self, syntactic_iterable, iterator, new_expr):
+    def _get_for_iterators(self, syntactic_iterable, iterator, new_expr, expr):
         """
         Get the semantic target and iterable of a for loop.
 
@@ -2253,6 +2291,8 @@ class SemanticParser(BasicParser):
         new_expr : list[PyccelAstNode]
             A list which allows collection of any additional expressions
             resulting from this operation (e.g. Allocation).
+        expr : PyccelAstNode
+            The expression being visited. This is used for error handling.
 
         Returns
         -------
@@ -2305,6 +2345,10 @@ class SemanticParser(BasicParser):
 
             target = self._assign_lhs_variable(iterator, iterator_d_var,
                             rhs=iterator_rhs, new_expressions=new_expr)
+
+            if target.is_alias:
+                self._indicate_pointer_target(target, iterator_rhs, expr.python_ast)
+
             if isinstance(target.class_type, InhomogeneousTupleType):
                 target = [self.scope.collect_tuple_element(v) for v in target]
             else:
@@ -2314,6 +2358,10 @@ class SemanticParser(BasicParser):
             target = [self._assign_lhs_variable(it, self._infer_type(rhs),
                                 rhs=rhs, new_expressions=new_expr)
                         for it, rhs in zip(iterator, iterator_rhs)]
+
+            for t, rhs in zip(target, iterator_rhs):
+                if t.is_alias:
+                    self._indicate_pointer_target(t, rhs, expr.python_ast)
         else:
             raise errors.report(INVALID_FOR_ITERABLE, symbol=iterator,
                    bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
@@ -2658,7 +2706,7 @@ class SemanticParser(BasicParser):
         if isinstance(value, (PyccelArithmeticOperator, PyccelFunction)) and value.rank:
             a = generate_and_assign_temp_var()
         elif isinstance(value, FunctionCall) and isinstance(value.class_type, CustomDataType):
-            if not value.funcdef.results.var.is_alias:
+            if value.funcdef.results.var and not value.funcdef.results.var.is_alias:
                 a = generate_and_assign_temp_var()
         return a
 
@@ -3375,7 +3423,10 @@ class SemanticParser(BasicParser):
                             symbol=annotation, severity='error')
                 lhs = lhs.name
 
-            cls_variable = self._assign_lhs_variable(lhs, d_var, expr, new_expression, False)
+            cls_variable = self._assign_lhs_variable(lhs, d_var,
+                                    rhs = method.results.var,
+                                    new_expressions = new_expression,
+                                    is_augassign = False)
             self._additional_exprs[-1].extend(new_expression)
             args = (FunctionCallArgument(cls_variable), *args)
             self._check_argument_compatibility(args, method.arguments,
@@ -3867,8 +3918,10 @@ class SemanticParser(BasicParser):
 
                 elif isinstance(l.class_type, SymbolicType):
                     errors.report(PYCCEL_RESTRICTION_TODO,
-                                  bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
+                                  symbol=expr,
                                   severity='fatal')
+                elif isinstance(r, (PythonList, PythonSet, PythonTuple, PythonDict)):
+                    self._indicate_pointer_target(l, r, expr)
 
             new_expressions.append(new_expr)
 
@@ -3949,7 +4002,7 @@ class SemanticParser(BasicParser):
         new_expr = []
 
         # treatment of the index/indices
-        target, iterable = self._get_for_iterators(expr.iterable, expr.target, new_expr)
+        target, iterable = self._get_for_iterators(expr.iterable, expr.target, new_expr, expr)
 
         body = self._visit(expr.body)
 
@@ -4726,17 +4779,13 @@ class SemanticParser(BasicParser):
             for r in results_vars:
                 t = pointer_targets.get(r, ())
                 if r.is_alias:
-                    persistent_targets = []
-                    for target, _ in t:
-                        target_argument_index = next((i for i,a in enumerate(arguments) if a.var == target), -1)
-                        if target_argument_index != -1:
-                            persistent_targets.append(target_argument_index)
-                    if not persistent_targets:
+                    arg_vars = [a.var for a in arguments]
+                    temp_targets = [target for target, _ in t if target not in arg_vars]
+                    if temp_targets:
                         errors.report(UNSUPPORTED_POINTER_RETURN_VALUE,
-                            symbol=r, severity='error',
-                            bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset))
+                            symbol=r, severity='error')
                     else:
-                        result_pointer_map[r] = persistent_targets
+                        result_pointer_map[r] = [next(i for i,a in enumerate(arguments) if a.var == target) for target, _ in t]
 
             optional_inits = []
             for a in arguments:
@@ -5384,7 +5433,7 @@ class SemanticParser(BasicParser):
         Parameters
         ----------
         expr : DottedName
-            The syntactic DottedName node that represent the call to `.extend()`.
+            The syntactic DottedName node that represents the call to `.extend()`.
 
         args : iterable[FunctionCallArgument]
             The semantic arguments passed to the function.
@@ -5404,6 +5453,10 @@ class SemanticParser(BasicParser):
             except TypeError as e:
                 msg = str(e)
                 errors.report(msg, symbol=expr, severity='fatal')
+            if not isinstance(list_variable.class_type.element_type, (StringType, FixedSizeNumericType)):
+                for a in added_list:
+                    if not isinstance(a, (PythonList, PythonSet, PythonTuple, NumpyNewArray)):
+                        self._indicate_pointer_target(list_variable, a, expr)
             return CodeBlock(store)
         else:
             pyccel_stage.set_stage('syntactic')
@@ -5411,6 +5464,7 @@ class SemanticParser(BasicParser):
             arg = FunctionCallArgument(for_target)
             func_call = FunctionCall('append', [arg])
             dotted = DottedName(expr.name[0], func_call)
+            dotted.set_current_ast(expr.python_ast)
             lhs = PyccelSymbol('_', is_temp=True)
             assign = Assign(lhs, dotted)
             assign.set_current_ast(expr.python_ast)
@@ -5434,7 +5488,7 @@ class SemanticParser(BasicParser):
             The syntactic FunctionCall describing the call to `cmath.sqrt`.
 
         func_call_args : iterable[FunctionCallArgument]
-            The semantic arguments passed to the function.
+            The semantic argument passed to the function.
 
         Returns
         -------
@@ -5491,7 +5545,7 @@ class SemanticParser(BasicParser):
             The syntactic FunctionCall describing the call to `cmath.sqrt`.
 
         func_call_args : iterable[FunctionCallArgument]
-            The semantic arguments passed to the function.
+            The semantic argument passed to the function.
 
         Returns
         -------
@@ -5541,7 +5595,7 @@ class SemanticParser(BasicParser):
             The syntactic FunctionCall describing the call to `cmath.polar`.
 
         func_call_args : iterable[FunctionCallArgument]
-            The semantic arguments passed to the function.
+            The semantic argument passed to the function.
 
         Returns
         -------
@@ -5578,7 +5632,7 @@ class SemanticParser(BasicParser):
             The syntactic FunctionCall describing the call to `cmath.rect`.
 
         func_call_args : iterable[FunctionCallArgument]
-            The semantic arguments passed to the function.
+            The 2 semantic arguments passed to the function.
 
         Returns
         -------
@@ -5608,7 +5662,7 @@ class SemanticParser(BasicParser):
             The syntactic FunctionCall describing the call to `cmath.phase`.
 
         func_call_args : iterable[FunctionCallArgument]
-            The semantic arguments passed to the function.
+            The semantic argument passed to the function.
 
         Returns
         -------
@@ -5738,7 +5792,7 @@ class SemanticParser(BasicParser):
         Parameters
         ----------
         expr : DottedName | AugAssign
-            The syntactic DottedName node that represent the call to `.update()`.
+            The syntactic DottedName node that represents the call to `.update()`.
 
         args : iterable[FunctionCallArgument]
             The semantic arguments passed to the function.
@@ -5798,7 +5852,7 @@ class SemanticParser(BasicParser):
         Parameters
         ----------
         expr : DottedName
-            The syntactic DottedName node that represent the call to `.union()`.
+            The syntactic DottedName node that represents the call to `.union()`.
 
         function_call_args : iterable[FunctionCallArgument]
             The semantic arguments passed to the function.
@@ -5853,7 +5907,7 @@ class SemanticParser(BasicParser):
         Parameters
         ----------
         expr : DottedName
-            The syntactic DottedName node that represent the call to `.intersection()`.
+            The syntactic DottedName node that represents the call to `.intersection()`.
 
         function_call_args : iterable[FunctionCallArgument]
             The semantic arguments passed to the function.
@@ -5900,10 +5954,10 @@ class SemanticParser(BasicParser):
         Parameters
         ----------
         expr : FunctionCall
-            The syntactic node that represent the call to `len()`.
+            The syntactic node that represents the call to `len()`.
 
         function_call_args : iterable[FunctionCallArgument]
-            The semantic arguments passed to the function.
+            The semantic argument passed to the function.
 
         Returns
         -------
@@ -5941,7 +5995,7 @@ class SemanticParser(BasicParser):
         Parameters
         ----------
         expr : FunctionCall
-            The syntactic node that represent the call to `PythonSetFunction`.
+            The syntactic node that represents the call to `PythonSetFunction`.
 
         function_call_args : iterable[FunctionCallArgument]
             The semantic arguments passed to the function.
@@ -5976,7 +6030,7 @@ class SemanticParser(BasicParser):
             body = []
             lhs_semantic_var = self._assign_lhs_variable(lhs, d_var, PythonSetFunction(arg), body)
             scope = self.create_new_loop_scope()
-            targets, iterable = self._get_for_iterators(arg, self.scope.get_new_name(), body)
+            targets, iterable = self._get_for_iterators(arg, self.scope.get_new_name(), body, expr)
             self.exit_loop_scope()
             body.append(For(targets, iterable, [SetAdd(lhs_semantic_var, targets[0])], scope=scope))
             if assigns:
@@ -5991,15 +6045,15 @@ class SemanticParser(BasicParser):
 
         The purpose of this `_build` method is to construct a literal boolean indicating
         whether or not the expression has the expected type.
-            The syntactic node that represent the call to `isinstance()`.
+        The syntactic node that represents the call to `isinstance()`.
 
         Parameters
         ----------
         expr : FunctionCall
-            The syntactic node that represent the call to `PythonSetFunction`.
+            The syntactic node that represents the call to `PythonSetFunction`.
 
         function_call_args : iterable[FunctionCallArgument]
-            The semantic arguments passed to the function.
+            The 2 semantic arguments passed to the function.
 
         Returns
         -------
@@ -6048,3 +6102,57 @@ class SemanticParser(BasicParser):
                 errors.report(f"Type {class_or_tuple} is not handled in isinstance call.",
                         severity='error', symbol=expr)
                 return LiteralTrue()
+
+    def _build_ListAppend(self, expr, args):
+        """
+        Method to create the semantic ListAppend node.
+
+        Method to create the semantic ListAppend node ensuring that pointers are
+        correctly handled.
+
+        Parameters
+        ----------
+        expr : DottedName
+            The syntactic DottedName node that represents the call to `.append()`.
+
+        args : iterable[FunctionCallArgument]
+            An iterable containing the 1 semantic argument passed to the function.
+
+        Returns
+        -------
+        ListAppend
+            The semantic ListAppend object.
+        """
+        list_obj, append_arg = [a.value for a in args]
+        semantic_node = ListAppend(list_obj, append_arg)
+        if not isinstance(append_arg.class_type, (StringType, FixedSizeNumericType)) \
+                and not isinstance(append_arg, (PythonList, PythonSet, PythonTuple, NumpyNewArray)):
+            self._indicate_pointer_target(list_obj, append_arg, expr)
+        return semantic_node
+
+    def _build_ListInsert(self, expr, args):
+        """
+        Method to create the semantic ListInsert node.
+
+        Method to create the semantic ListInsert node ensuring that pointers are
+        correctly handled.
+
+        Parameters
+        ----------
+        expr : DottedName
+            The syntactic DottedName node that represents the call to `.insert()`.
+
+        args : iterable[FunctionCallArgument]
+            The 2 semantic arguments passed to the function.
+
+        Returns
+        -------
+        ListInsert
+            The semantic ListInsert object.
+        """
+        list_obj, index, new_elem = [a.value for a in args]
+        semantic_node = ListInsert(list_obj, index, new_elem)
+        if not isinstance(new_elem.class_type, (StringType, FixedSizeNumericType)) \
+                and not isinstance(new_elem, (PythonList, PythonSet, PythonTuple, NumpyNewArray)):
+            self._indicate_pointer_target(list_obj, new_elem, expr)
+        return semantic_node


Test patch:
diff --git a/.github/actions/pytest_run/action.yml b/.github/actions/pytest_run/action.yml
index 3e51e67d..cc6e335a 100644
--- a/.github/actions/pytest_run/action.yml
+++ b/.github/actions/pytest_run/action.yml
@@ -63,7 +63,9 @@ runs:
       id: pytest_4
     - name: Test multi-file Fortran translations
       run: |
-        python -m pytest -rX ${FLAGS} -m "xdist_incompatible and not parallel and not (c or python) ${{ inputs.pytest_mark }}" 2>&1 | tee s5_outfile.out
+        # Temporarily run warnings separately as lists of lists are used in errors and singleton means warning is only raised once
+        python -m pytest -rX ${FLAGS} -m "xdist_incompatible and not parallel and not (c or python) ${{ inputs.pytest_mark }}" --ignore=warnings 2>&1 | tee s5_outfile.out
+        python -m pytest -rX ${FLAGS} -m "xdist_incompatible and not parallel and not (c or python) ${{ inputs.pytest_mark }}" warnings 2>&1 | tee -a s5_outfile.out
         pyccel-clean
       shell: ${{ inputs.shell_cmd }}
       working-directory: ./tests
diff --git a/tests/epyccel/modules/list_user_defined_objs.py b/tests/epyccel/modules/list_user_defined_objs.py
index 59e92023..c5de6d12 100644
--- a/tests/epyccel/modules/list_user_defined_objs.py
+++ b/tests/epyccel/modules/list_user_defined_objs.py
@@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst.insert(0, d)
     lst.insert(1, e)
-    return lst
+    return len(lst)
diff --git a/tests/epyccel/modules/list_user_defined_objs1.py b/tests/epyccel/modules/list_user_defined_objs1.py
index fbbcda8a..a6c987d9 100644
--- a/tests/epyccel/modules/list_user_defined_objs1.py
+++ b/tests/epyccel/modules/list_user_defined_objs1.py
@@ -5,12 +5,7 @@ class A:
         self.x = x
 
 def fn():
-    a = A(1)
-    b = A(2)
-    c = A(3)
-    d = A(4)
-    e = A(5)
-    lst = [a, b, c]
-    lst.append(d)
-    lst.append(e, e)
+    lst = [A(1), A(2), A(3)]
+    lst.append(A(4))
+    lst.append(A(5))
     return lst
diff --git a/tests/epyccel/modules/list_user_defined_objs2.py b/tests/epyccel/modules/list_user_defined_objs2.py
index a489c9fd..285512b0 100644
--- a/tests/epyccel/modules/list_user_defined_objs2.py
+++ b/tests/epyccel/modules/list_user_defined_objs2.py
@@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst2 = [d, e]
     lst.extend(lst2)
-    return lst
+    return len(lst)
diff --git a/tests/epyccel/modules/loops.py b/tests/epyccel/modules/loops.py
index 7de5c370..90fb510a 100644
--- a/tests/epyccel/modules/loops.py
+++ b/tests/epyccel/modules/loops.py
@@ -272,3 +272,14 @@ def for_expression():
         sum_e = ei
 
     return sum_e
+
+def for_lists_of_lists():
+    a = [[1,2], [3,4]]
+    b = [[5,6], [7,8]]
+    c = [0, 0]
+    for ai, bi in zip(a, b):
+        for i in range(2):
+            c[i] = ai[i] + bi[i]
+            bi[i] = -1
+
+    return c[0], c[1], b[0][0], b[0][1], b[1][0], b[1][1]
diff --git a/tests/epyccel/test_epyccel_lists.py b/tests/epyccel/test_epyccel_lists.py
index fe688d11..af00c66a 100644
--- a/tests/epyccel/test_epyccel_lists.py
+++ b/tests/epyccel/test_epyccel_lists.py
@@ -75,7 +75,8 @@ def test_pop_list_of_ndarrays(limited_language) :
         array2 = array([[7, 8, 9], [10, 11, 12]])
         array3 = array([[13, 14, 15], [16, 17, 18]])
         a = [array1, array2, array3]
-        return a.pop()
+        r = array(a.pop())
+        return r
     epyc_last_element = epyccel(pop_last_element, language = limited_language)
     pyccel_result = epyc_last_element()
     python_result = pop_last_element()
@@ -230,20 +231,20 @@ def test_append_ndarrays(limited_language):
         a.append(array4)
         a.append(array5)
         a.append(array6)
-        return a
+        return len(a), a[0][0,0], a[-1][1,0]
 
     epyc_f = epyccel(f, language=limited_language)
-    assert np.array_equal(f(), epyc_f())
+    assert f() == epyc_f()
 
 def test_append_user_defined_objects(limited_language):
-    import modules.list_user_defined_objs2 as mod
+    import modules.list_user_defined_objs1 as mod
 
     modnew = epyccel(mod, language=limited_language)
     python_list = mod.fn()
     accelerated_list = modnew.fn()
     assert len(python_list) == len(accelerated_list)
-    for python_elem, accelerated_elem in zip(python_list, accelerated_list):
-        assert python_elem.x == accelerated_elem.x
+    for pi, ai in zip(python_list, accelerated_list):
+        assert pi.x == ai.x
 
 def test_insert_basic(limited_language):
     def f():
@@ -314,10 +315,10 @@ def test_insert_ndarrays(limited_language):
         a.insert(0, array4)
         a.insert(100, array5)
         a.insert(-3, array6)
-        return a
+        return len(a), a[0][0,1], a[-1][1,2]
 
     epyc_f = epyccel(f, language=limited_language)
-    assert np.array_equal(f(), epyc_f())
+    assert f() == epyc_f()
 
 def test_insert_multiple(limited_language):
     def f():
@@ -365,9 +366,7 @@ def test_insert_user_defined_objects(limited_language):
     modnew = epyccel(mod, language=limited_language)
     python_list = mod.fn()
     accelerated_list = modnew.fn()
-    assert len(python_list) == len(accelerated_list)
-    for python_elem, accelerated_elem in zip(python_list, accelerated_list):
-        assert python_elem.x == accelerated_elem.x
+    assert python_list == accelerated_list
 
 def test_clear_1(language):
 
@@ -571,9 +570,7 @@ def test_extend_user_defined_objects(limited_language):
     modnew = epyccel(mod, language=limited_language)
     python_list = mod.fn()
     accelerated_list = modnew.fn()
-    assert len(python_list) == len(accelerated_list)
-    for python_elem, accelerated_elem in zip(python_list, accelerated_list):
-        assert python_elem.x == accelerated_elem.x
+    assert python_list == accelerated_list
 
 def test_remove_basic(limited_language):
     def f():
diff --git a/tests/epyccel/test_loops.py b/tests/epyccel/test_loops.py
index c97d309c..c105a526 100644
--- a/tests/epyccel/test_loops.py
+++ b/tests/epyccel/test_loops.py
@@ -251,3 +251,21 @@ def test_for_expression(language):
     f2 = epyccel( f1, language = language )
 
     assert f1() == f2()
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [
+            pytest.mark.skip(reason="lists of lists not yet implemented in Fortran. See #2210"),
+            pytest.mark.fortran]
+        ),
+        pytest.param("c", marks = [
+            pytest.mark.skip(reason="lists of lists not yet implemented in C. See #2210"),
+            pytest.mark.c]
+        ),
+        pytest.param("python", marks = pytest.mark.python)
+    )
+)
+def test_for_lists_of_lists(language):
+    f1 = loops.for_lists_of_lists
+    f2 = epyccel( f1, language = language )
+
+    assert f1() == f2()
diff --git a/tests/errors/semantic/non_blocking/RETURN_LIST_ELEMENT.py b/tests/errors/semantic/non_blocking/RETURN_LIST_ELEMENT.py
new file mode 100644
index 00000000..6c5129fb
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/RETURN_LIST_ELEMENT.py
@@ -0,0 +1,6 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+def f():
+    b = [[1,2,3], [4,5,6]]
+    c = b[0]
+    return c
diff --git a/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST.py b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST.py
new file mode 100644
index 00000000..cfbcb2f9
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST.py
@@ -0,0 +1,6 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+def f():
+    a = [1, 2, 3]
+    b = [a]
+    return b
diff --git a/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST2.py b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST2.py
new file mode 100644
index 00000000..33f4f0f6
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST2.py
@@ -0,0 +1,7 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+def f():
+    a = [1, 2, 3]
+    b = [[4,5,6]]
+    b.append(a)
+    return b
diff --git a/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST3.py b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST3.py
new file mode 100644
index 00000000..5ae971d9
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST3.py
@@ -0,0 +1,8 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+def f():
+    a = [1, 2, 3]
+    b = [7, 8, 9]
+    c = [[4,5,6]]
+    c.extend([a, b])
+    return c
diff --git a/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST4.py b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST4.py
new file mode 100644
index 00000000..e58173f4
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST4.py
@@ -0,0 +1,7 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+def f():
+    a = [1, 2, 3]
+    b = [[4,5,6]]
+    b.insert(0, a)
+    return b
diff --git a/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST_ELEMENT.py b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST_ELEMENT.py
new file mode 100644
index 00000000..6d39dec0
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/RETURN_NON_OWNING_LIST_ELEMENT.py
@@ -0,0 +1,8 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+def f():
+    a = [1, 2, 3]
+    b = [a]
+    c = b.pop()
+    return c
+