Repository: pyccel/pyccel
Pull request #2258: Add the necessary pointer information to ensure errors are raised for multi-level containers
Author: login: EmilyBourne
Labels: name: Ready_to_merge
description: Approved by senior developer. Ready for final approval and merge
Created: 2025-03-24T17:17:00Z | Merged: 2025-03-29T22:02:19Z

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 .


Diff:
diff --git a/.github/actions/pytest_run/action.yml b/.github/actions/pytest_run/action.yml
index 3e51e67d30..cc6e335a43 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/CHANGELOG.md b/CHANGELOG.md
index 902ac54b25..a23058e95b 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 768e9f75a9..54c694abfb 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -819,6 +819,15 @@ def allows_negative_indexes(self):
         """
         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 d66d1c574e..e02a354c1d 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -1237,7 +1237,10 @@ def _print_ConstructorCall(self, expr):
         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 3854bbe8e6..ab3d1e2abb 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -36,7 +36,7 @@
 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 @@ def _indicate_pointer_target(self, pointer, target, expr):
             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 @@ def _indicate_pointer_target(self, pointer, target, expr):
             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 @@ def _handle_function(self, expr, func, args, *, is_method = False, use_build_fun
                             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 @@ def _ensure_target(self, rhs, d_lhs):
             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 @@ def _ensure_inferred_type_matches_existing(self, class_type, d_var, var, is_auga
 
                     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 @@ def _assign_GeneratorComprehension(self, lhs_name, expr):
         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 @@ def _get_iterable(self, syntactic_iterable):
 
         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 @@ def _get_for_iterators(self, syntactic_iterable, iterator, new_expr):
         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 @@ def _get_for_iterators(self, syntactic_iterable, iterator, new_expr):
 
             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 @@ def _get_for_iterators(self, syntactic_iterable, iterator, new_expr):
             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 @@ def generate_and_assign_temp_var():
         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 @@ def _visit_FunctionCall(self, expr):
                             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 @@ def _visit_Assign(self, expr):
 
                 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 @@ def _visit_For(self, expr):
         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 @@ def unpack(ann):
             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 @@ def _build_ListExtend(self, expr, args):
         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 @@ def _build_ListExtend(self, expr, args):
             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 @@ def _build_ListExtend(self, expr, args):
             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 @@ def _build_MathSqrt(self, func_call, func_call_args):
             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 @@ def _build_CmathSqrt(self, func_call, func_call_args):
             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 @@ def _build_CmathPolar(self, func_call, func_call_args):
             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 @@ def _build_CmathRect(self, func_call, func_call_args):
             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 @@ def _build_CmathPhase(self, func_call, func_call_args):
             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 @@ def _build_SetUpdate(self, expr, args):
         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 @@ def _build_SetUnion(self, expr, function_call_args):
         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 @@ def _build_SetIntersection(self, expr, function_call_args):
         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 @@ def _build_PythonLen(self, expr, function_call_args):
         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 @@ def _build_PythonSetFunction(self, expr, function_call_args):
         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 @@ def _build_PythonSetFunction(self, expr, function_call_args):
             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 @@ def _build_PythonIsInstance(self, expr, function_call_args):
 
         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 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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
diff --git a/tests/epyccel/modules/list_user_defined_objs.py b/tests/epyccel/modules/list_user_defined_objs.py
index 59e92023d0..c5de6d127c 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 fbbcda8ad2..a6c987d991 100644
--- a/tests/epyccel/modules/list_user_defined_objs1.py
+++ b/tests/epyccel/modules/list_user_defined_objs1.py
@@ -5,12 +5,7 @@ def __init__(self, x : int):
         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 a489c9fd8b..285512b00f 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 7de5c370b2..90fb510a32 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 fe688d11ec..af00c66a1d 100644
--- a/tests/epyccel/test_epyccel_lists.py
+++ b/tests/epyccel/test_epyccel_lists.py
@@ -75,7 +75,8 @@ def pop_last_element():
         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 f():
         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 f():
         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 c97d309c2b..c105a5263d 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 0000000000..6c5129fb43
--- /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 0000000000..cfbcb2f9d6
--- /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 0000000000..33f4f0f6f9
--- /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 0000000000..5ae971d94d
--- /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 0000000000..e58173f494
--- /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 0000000000..6d39dec0e4
--- /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
+


Reviews:
user: login: pyccel-bot[bot]
body: There seems to be lines in this PR which aren't tested. Please take a look at my comments and add tests which cover the new code.

If this is modified code which cannot be easily tested in this PR please open an issue to request that this code be either removed or tested. Once you have done that please leave a message on the relevant conversation beginning with the line `/bot accept` and referencing the issue.

Similarly if the new code cannot be tested for some reason, please leave a comment beginning with the line `/bot accept` on the relevant conversation explaining why the code can't be tested.


state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-25T10:41:09Z
user: login: pyccel-bot[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T07:36:57Z
user: login: pyccel-bot[bot]
body: Good job ! Your PR is using all the code it added/changed.

state: APPROVED
author_association: NONE
submitted_at: 2025-03-26T07:36:57Z
user: login: pyccel-bot[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T07:36:58Z
user: login: pyccel-bot[bot]
body: Good job ! Your PR is using all the code it added/changed.

state: APPROVED
author_association: NONE
submitted_at: 2025-03-26T07:38:03Z
user: login: yguclu
body: I have a few questions
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T10:44:46Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T13:36:41Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T13:39:41Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T13:42:42Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T13:43:33Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T13:46:51Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T13:48:21Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T14:05:38Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T14:07:09Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T14:07:49Z
user: login: yguclu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T18:18:13Z
user: login: yguclu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T18:20:03Z
user: login: yguclu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T18:29:40Z
user: login: yguclu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T19:23:22Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T06:17:01Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T09:17:52Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T09:21:10Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T09:21:14Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T09:21:17Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T09:21:20Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T09:22:36Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T09:23:29Z
user: login: yguclu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T13:57:01Z
user: login: yguclu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T14:01:17Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T15:04:12Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T15:06:18Z
user: login: yguclu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T16:42:45Z
user: login: yguclu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T16:55:44Z
user: login: EmilyBourne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T17:03:37Z
user: login: yguclu
body: Looks good!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-29T17:18:35Z
user: login: pyccel-bot[bot]
body: Good job ! Your PR is using all the code it added/changed.

state: APPROVED
author_association: NONE
submitted_at: 2025-03-29T17:44:05Z

Review comments:
diff_hunk: @@ -2314,6 +2358,10 @@ def _get_for_iterators(self, syntactic_iterable, iterator, new_expr):
             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)
path: pyccel/parser/semantic.py
user: login: pyccel-bot[bot]
body: This code isn't tested. Please can you take a look
created_at: 2025-03-25T10:41:09Z
author_association: NONE
diff_hunk: @@ -3867,8 +3918,20 @@
 
                 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)
+                elif isinstance(r, (ListPop, DictPop, DictPopitem)) and \
+                        not isinstance(l.class_type, (StringType, FixedSizeNumericType)):
+                    class_obj = getattr(r, 'dict_obj', getattr(r, 'list_obj', None))
+                    class_obj = r.list_obj if isinstance(r, ListPop) else r.dict_obj
+                    for target, target_expr in self._pointer_targets[-1].get(class_obj, ()):
+                        # Create an expr describing 2 lines to show where the dependency comes from
+                        indicated_expr = CodeBlock([target_expr, expr])
+                        # Show the line number of the assignment for the returned object
+                        indicated_expr.set_current_ast(expr.python_ast)
+                        self._indicate_pointer_target(l, target, indicated_expr)
path: pyccel/parser/semantic.py
user: login: pyccel-bot[bot]
body: This code isn't tested. Please can you take a look
created_at: 2025-03-25T10:41:09Z
author_association: NONE
diff_hunk: @@ -3867,8 +3918,20 @@
 
                 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)
+                elif isinstance(r, (ListPop, DictPop, DictPopitem)) and \
+                        not isinstance(l.class_type, (StringType, FixedSizeNumericType)):
+                    class_obj = getattr(r, 'dict_obj', getattr(r, 'list_obj', None))
+                    class_obj = r.list_obj if isinstance(r, ListPop) else r.dict_obj
+                    for target, target_expr in self._pointer_targets[-1].get(class_obj, ()):
+                        # Create an expr describing 2 lines to show where the dependency comes from
+                        indicated_expr = CodeBlock([target_expr, expr])
+                        # Show the line number of the assignment for the returned object
+                        indicated_expr.set_current_ast(expr.python_ast)
+                        self._indicate_pointer_target(l, target, indicated_expr)
path: pyccel/parser/semantic.py
user: login: pyccel-bot[bot]
body: This seems to be fixed now! Thank you for looking into this. Please resolve this conversation.

created_at: 2025-03-26T07:36:56Z
author_association: NONE
diff_hunk: @@ -2314,6 +2358,10 @@ def _get_for_iterators(self, syntactic_iterable, iterator, new_expr):
             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)
path: pyccel/parser/semantic.py
user: login: pyccel-bot[bot]
body: This seems to be fixed now! Thank you for looking into this. Please resolve this conversation.

created_at: 2025-03-26T07:36:58Z
author_association: NONE
diff_hunk: @@ -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
path: .github/actions/pytest_run/action.yml
user: login: yguclu
body: Could you please elaborate on this comment a bit?
created_at: 2025-03-28T10:10:09Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: yguclu
body: Maybe we can say what kind of errors these are? Errors due to no current implementation?
created_at: 2025-03-28T10:12:24Z
author_association: MEMBER
diff_hunk: @@ -819,6 +819,16 @@ def allows_negative_indexes(self):
         """
         return self.base.allows_negative_indexes
 
+    @property
+    def is_slice(self):
+        """
+        Indicates whether the result is a slice of a container.
+
+        Indicates whether the result is a slice of a container or an element
+        of a container.
path: pyccel/ast/variable.py
user: login: yguclu
body: What do you mean with "result" exactly?

Further, I may be confused by the use of "whether... or" in this sentence. As this is an indexed element, I suppose that it should always represent a slice or an element.
created_at: 2025-03-28T10:22:12Z
author_association: MEMBER
diff_hunk: @@ -1237,7 +1237,10 @@ def _print_ConstructorCall(self, expr):
         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})"
path: pyccel/codegen/printing/pycode.py
user: login: yguclu
body: Are you removing an unnecessary temporary variable here?
created_at: 2025-03-28T10:23:42Z
author_association: MEMBER
diff_hunk: @@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst.insert(0, d)
     lst.insert(1, e)
-    return lst
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs.py
user: login: yguclu
body: Why do you only return the length of the list now?
created_at: 2025-03-28T10:29:15Z
author_association: MEMBER
diff_hunk: @@ -10,7 +10,7 @@ def fn():
     c = A(3)
     d = A(4)
     e = A(5)
-    lst = [a, b, c]
-    lst.append(d)
-    lst.append(e, e)
-    return lst
+    lst = [A(1), A(2), A(3)]
+    lst.append(A(4))
+    lst.append(A(5))
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs1.py
user: login: yguclu
body: What is the reason for these changes? `lst.append(e, e)` is not correct Python, but the rest looks legit to me.
created_at: 2025-03-28T10:34:11Z
author_association: MEMBER
diff_hunk: @@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst2 = [d, e]
     lst.extend(lst2)
-    return lst
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs2.py
user: login: yguclu
body: Again, why returning just the length instead of the whole list?
created_at: 2025-03-28T10:35:50Z
author_association: MEMBER
diff_hunk: @@ -75,7 +75,8 @@ def pop_last_element():
         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
path: tests/epyccel/test_epyccel_lists.py
user: login: yguclu
body: Do you need to make a copy?
created_at: 2025-03-28T10:43:55Z
author_association: MEMBER
diff_hunk: @@ -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
path: .github/actions/pytest_run/action.yml
user: login: EmilyBourne
body: Currently there is this test in the warnings folder:
https://github.com/pyccel/pyccel/blob/67753bde78c9620c521e9e893fa6a1beedb30d8b/tests/warnings/semantic/NESTED_LISTS.py#L1-L4
The warning is raised here:
https://github.com/pyccel/pyccel/blob/67753bde78c9620c521e9e893fa6a1beedb30d8b/pyccel/ast/datatypes.py#L819-L821

The class `HomogeneousListType` is a singleton so if any other tests (e.g. the new error tests) use lists of lists then the warning is raised in the first file that uses this type. If we run pytest twice then the warning is raised twice.

This is a temporary solution as the PR after this which adds proper support for lists of lists will remove the warning.
created_at: 2025-03-28T13:36:40Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: EmilyBourne
body: The errors that were missing previously are the ones added by this PR which are tested in the new files in the errors/ folder. E.g.
```
ERROR at annotation (semantic) stage
pyccel:
 |warning [semantic]: RETURN_LIST_ELEMENT.py| Lists of non-scalar objects are not yet fully supported. Using containers in lists may lead to bugs such as memory leaks (list[list[int]])
 |error [semantic]: RETURN_LIST_ELEMENT.py [5,4]| Variable b goes out of scope but may be the target of a pointer which is still required (c := b[0])
 |error [semantic]: RETURN_LIST_ELEMENT.py| A pointer can only be returned from a function if it points at one of the arguments. (c)
```

The errors themselves existed before this PR, but they were not raised for containers of containers.
created_at: 2025-03-28T13:39:41Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: EmilyBourne
body: On the main branch most of the files don't translate due to #1945 but it is possible to construct an example that compiles.
E.g. on the main branch:
```python
def f():
    a = [1,2,3]
    e = [4,5,6]
    b = [a, e]
    c = b[0]
    return c
```
translates to a C file that compiles but trying to use this function should lead to a seg fault (I think currently there are copies that mean that it is not as dangerous)
created_at: 2025-03-28T13:42:41Z
author_association: MEMBER
diff_hunk: @@ -819,6 +819,16 @@ def allows_negative_indexes(self):
         """
         return self.base.allows_negative_indexes
 
+    @property
+    def is_slice(self):
+        """
+        Indicates whether the result is a slice of a container.
+
+        Indicates whether the result is a slice of a container or an element
+        of a container.
path: pyccel/ast/variable.py
user: login: EmilyBourne
body: I just mean the object described by the class.
```suggestion
        Indicates whether this is a slice of a container or an element
        of a container.
```
```suggestion
        Indicates whether this instance represents a slice of a container
        or an element of a container.
```
?
created_at: 2025-03-28T13:43:33Z
author_association: MEMBER
diff_hunk: @@ -1237,7 +1237,10 @@ def _print_ConstructorCall(self, expr):
         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})"
path: pyccel/codegen/printing/pycode.py
user: login: EmilyBourne
body: Sort of. This appears when I do:
```python
a = [A(1), A(2)]
```
on the devel branch this translates to:
```python
a = [Dummy_0000 = A(1)
    , Dummy_0001 = A(2)
    , Dummy_0002 = A(3)
    ]
```
I think we must have assumed that it was not possible to call the constructor without being in an `Assign` node. But here we must not create an `Assign` node otherwise the list will not be responsible for the memory.
created_at: 2025-03-28T13:46:50Z
author_association: MEMBER
diff_hunk: @@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst.insert(0, d)
     lst.insert(1, e)
-    return lst
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs.py
user: login: EmilyBourne
body: The previous code was not valid. Following this PR if I try to translate the original file I get the following errors:
```
ERROR at annotation (semantic) stage
pyccel:
 |error [semantic]: list_user_defined_objs.py [13,4]| Variable c goes out of scope but may be the target of a pointer which is still required (lst := [a, b, c])
 |error [semantic]: list_user_defined_objs.py [13,4]| Variable a_0001 goes out of scope but may be the target of a pointer which is still required (lst := [a, b, c])
 |error [semantic]: list_user_defined_objs.py [13,4]| Variable b goes out of scope but may be the target of a pointer which is still required (lst := [a, b, c])
 |error [semantic]: list_user_defined_objs.py [15,4]| Variable e goes out of scope but may be the target of a pointer which is still required (lst.insert(1, e))
 |error [semantic]: list_user_defined_objs.py [14,4]| Variable d goes out of scope but may be the target of a pointer which is still required (lst.insert(0, d))
```
We cannot return `lst` if it contains pointers to objects that are deallocated
created_at: 2025-03-28T13:48:21Z
author_association: MEMBER
diff_hunk: @@ -10,7 +10,7 @@ def fn():
     c = A(3)
     d = A(4)
     e = A(5)
-    lst = [a, b, c]
-    lst.append(d)
-    lst.append(e, e)
-    return lst
+    lst = [A(1), A(2), A(3)]
+    lst.append(A(4))
+    lst.append(A(5))
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs1.py
user: login: EmilyBourne
body: The reason for this change was to avoid the `return len(lst)` (which I forgot to remove). I wanted to make `lst` own all the elements of the list so I can return the whole thing.

Fixed : 8b82fe1d07b28cee3b6d241b2ee0ca08d3427823
created_at: 2025-03-28T14:05:38Z
author_association: MEMBER
diff_hunk: @@ -75,7 +75,8 @@ def pop_last_element():
         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
path: tests/epyccel/test_epyccel_lists.py
user: login: EmilyBourne
body: Yes. If you don't make a copy then you will try to return `array3` which is deallocated at the end of this function
created_at: 2025-03-28T14:07:09Z
author_association: MEMBER
diff_hunk: @@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst2 = [d, e]
     lst.extend(lst2)
-    return lst
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs2.py
user: login: EmilyBourne
body: As above, returning the list itself gives:
```
ERROR at annotation (semantic) stage
pyccel:
 |error [semantic]: list_user_defined_objs2.py [15,4]| Variable lst2 goes out of scope but may be the target of a pointer which is still required (lst.append(index))
 |error [semantic]: list_user_defined_objs2.py [13,4]| Variable c goes out of scope but may be the target of a pointer which is still required (lst := [a, b, c])
 |error [semantic]: list_user_defined_objs2.py [13,4]| Variable b goes out of scope but may be the target of a pointer which is still required (lst := [a, b, c])
 |error [semantic]: list_user_defined_objs2.py [13,4]| Variable a_0001 goes out of scope but may be the target of a pointer which is still required (lst := [a, b, c])
```
created_at: 2025-03-28T14:07:49Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: yguclu
body: Thanks for the explanation! To me it was not clear that by "container" you meant pretty much any object of a user-defined class...
created_at: 2025-03-28T18:18:13Z
author_association: MEMBER
diff_hunk: @@ -819,6 +819,16 @@ def allows_negative_indexes(self):
         """
         return self.base.allows_negative_indexes
 
+    @property
+    def is_slice(self):
+        """
+        Indicates whether the result is a slice of a container.
+
+        Indicates whether the result is a slice of a container or an element
+        of a container.
path: pyccel/ast/variable.py
user: login: yguclu
body: See my previous comment about the word "container" 
created_at: 2025-03-28T18:20:03Z
author_association: MEMBER
diff_hunk: @@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst.insert(0, d)
     lst.insert(1, e)
-    return lst
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs.py
user: login: yguclu
body: Is it not our decision to deallocate those objects when the variables `a b c d e` go out of scope?
created_at: 2025-03-28T18:29:40Z
author_association: MEMBER
diff_hunk: @@ -6048,3 +6102,57 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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 represent the call to `.append()`.
+
+        args : iterable[FunctionCallArgument]
+            The semantic arguments passed to the function.
path: pyccel/parser/semantic.py
user: login: yguclu
body: Here I would mention that `len(args) = 1`
created_at: 2025-03-28T19:12:30Z
author_association: MEMBER
diff_hunk: @@ -6048,3 +6102,57 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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 represent the call to `.append()`.
+
+        args : iterable[FunctionCallArgument]
+            The semantic arguments 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 represent the call to `.insert()`.
+
+        args : iterable[FunctionCallArgument]
+            The semantic arguments passed to the function.
path: pyccel/parser/semantic.py
user: login: yguclu
body: Here I would mention that `len(args) = 2`
created_at: 2025-03-28T19:13:40Z
author_association: MEMBER
diff_hunk: @@ -6048,3 +6102,57 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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 represent the call to `.append()`.
+
+        args : iterable[FunctionCallArgument]
+            The semantic arguments 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 represent the call to `.insert()`.
path: pyccel/parser/semantic.py
user: login: yguclu
body: Typo
```suggestion
            The syntactic DottedName node that represents the call to `.insert()`.
```
created_at: 2025-03-28T19:18:06Z
author_association: MEMBER
diff_hunk: @@ -6048,3 +6102,57 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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 represent the call to `.append()`.
path: pyccel/parser/semantic.py
user: login: yguclu
body: Typo
```suggestion
            The syntactic DottedName node that represents the call to `.append()`.
```
created_at: 2025-03-28T19:19:23Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: EmilyBourne
body: By "container" I mean any object that contains other objects. In particular lists/sets/tuples/dicts. I don't usually mean user-defined classes here
created_at: 2025-03-29T06:17:01Z
author_association: MEMBER
diff_hunk: @@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst.insert(0, d)
     lst.insert(1, e)
-    return lst
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs.py
user: login: EmilyBourne
body: It is our decision but it is done for performance reasons. As regards pointers every variable can be one of three things:
1. A pointer to another Variable
2. A target of a pointer
3. Neither a pointer or a target

Bear in mind that in the following:
```python
a = np.array([1,2,3])
b = a[::2]
```
`b` is a pointer pointing at the target `a`. So unless we want to introduce a concept `is_low_performance_target` the two functions below need to raise the same error:
```python
def foo():
    a = np.array([1,2,3])
    b = a[::2]
    return b

def bar():
    a = [1,2,3]
    b = [a]
    return b
```
In the devel branch the function `foo` raises:
```
ERROR at annotation (semantic) stage
pyccel:
 |error [semantic]: tmp3.py [5,4]| Variable a goes out of scope but may be the target of a pointer which is still required (b := a[ :  : 2])
 |error [semantic]: tmp3.py [3]| A pointer can only be returned from a function if it points at one of the arguments. (b)
```
which is why I added the equivalent error here.

---

If we want to go in a different direction then I would recommend saying that as regards pointers every variable can be one of five things:
1. A pointer to another Variable
2. A target of a pointer
3. Neither a pointer or a target
4. A low-performance pointer
5. A low-performance target of a low-performance pointer

The 2 additional cases would be handled using reference counting.
E.g. the following code:
```python
def foo():
    a = np.array([1,2,3])
    b = a[::2]
    c = np.array([1,2,3])
    d = [c]
    return d
```
could be translated to:
```c
vec_array_int64_t_1d_mem foo(void)
{
    array_int64_t_1d a = {0};
    array_int64_t_1d b;
    array_int64_t_1d_mem c;
    vec_array_int64_t_1d_mem d = {0};
    int64_t* a_ptr;
    int64_t* c_ptr;
    a_ptr = malloc(sizeof(int64_t) * (INT64_C(3)));
    a = (array_int64_t_1d)cspan_md_layout(c_ROWMAJOR, a_ptr, INT64_C(3));
    (*cspan_at(&a, INT64_C(0))) = INT64_C(1);
    (*cspan_at(&a, INT64_C(1))) = INT64_C(2);
    (*cspan_at(&a, INT64_C(2))) = INT64_C(3);
    b = cspan_slice(array_int64_t_1d, &a, {INT64_C(0), c_END, INT64_C(2)});
    c_ptr = malloc(sizeof(int64_t) * (INT64_C(3)));
    c_mem = array_int64_t_1d_mem_make((array_int64_t_1d)cspan_md_layout(c_ROWMAJOR, c_ptr, INT64_C(3)));
    (*cspan_at(c.get, INT64_C(0))) = INT64_C(1);
    (*cspan_at(c.get, INT64_C(1))) = INT64_C(2);
    (*cspan_at(c.get, INT64_C(2))) = INT64_C(3);
    d = c_init(vec_array_int64_t_1d_mem, {c});
    free(a.data);
    a.data = NULL;
    array_int64_t_1d_mem_drop(&c); // decreases reference count
    return d;
}
```
This has the advantage of using the reference counting objects as designed (they are entirely responsible for their own memory), but it could be quite difficult to work out which type to use when mixing normal pointers and lists/sets/etc. E.g:
```python
1. def foo():
2.     a = np.array([1,2,3])
3.     b = a[::2]
4.     c = a
5.     d = [a]
```
Here `a` must be a low-performance (reference counted) target. This is recognised at line 5. If `a` is a low-performance target then `b` and `c` should be low-performance pointers, but this will not be detected when these lines are examined. Further `b` should be reference counted, but its target is not itself which means Python-style reference counting is required where each reference holds a list of the objects whose reference should be decremented when it is decremented.

I think there are a lot of corner cases in this configuration. E.g:
```
def boo():
    a = np.array([1,2,3])
    b = a[::2]
    c = [b] # a must not be deallocated if c is returned
```

So even if this solution is technically possible, I don't think that it is worth the added complexity.

---

How would you like this to be handled? Are you looking for a docs section explaining how scopes work in low-level languages? A different solution? Something else?
created_at: 2025-03-29T09:17:52Z
author_association: MEMBER
diff_hunk: @@ -6048,3 +6102,57 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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 represent the call to `.append()`.
+
+        args : iterable[FunctionCallArgument]
+            The semantic arguments passed to the function.
path: pyccel/parser/semantic.py
user: login: EmilyBourne
body: Fixed 61dfe7aec
created_at: 2025-03-29T09:21:10Z
author_association: MEMBER
diff_hunk: @@ -6048,3 +6102,57 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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 represent the call to `.append()`.
+
+        args : iterable[FunctionCallArgument]
+            The semantic arguments 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 represent the call to `.insert()`.
+
+        args : iterable[FunctionCallArgument]
+            The semantic arguments passed to the function.
path: pyccel/parser/semantic.py
user: login: EmilyBourne
body: Fixed 61dfe7aec
created_at: 2025-03-29T09:21:13Z
author_association: MEMBER
diff_hunk: @@ -6048,3 +6102,57 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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 represent the call to `.append()`.
+
+        args : iterable[FunctionCallArgument]
+            The semantic arguments 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 represent the call to `.insert()`.
path: pyccel/parser/semantic.py
user: login: EmilyBourne
body: Fixed 61dfe7aec
created_at: 2025-03-29T09:21:17Z
author_association: MEMBER
diff_hunk: @@ -6048,3 +6102,57 @@ def _build_PythonIsInstance(self, expr, function_call_args):
                 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 represent the call to `.append()`.
path: pyccel/parser/semantic.py
user: login: EmilyBourne
body: Fixed 61dfe7aec
created_at: 2025-03-29T09:21:20Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: EmilyBourne
body: Do you have a better suggestion? The word container is used in many places in the CHANGELOG. E.g:
```md
-   #2009 : Add support for `in` operator for `list`, `set`, `dict` and class containers.
-   #1874 : Add C and Fortran support for the `len()` function for the `list` container.
-   #1875 : Add C and Fortran support for the `len()` function for the `set` container.
-   #1908 : Add C and Fortran support for the `len()` function for the `dict` container.
```
created_at: 2025-03-29T09:22:36Z
author_association: MEMBER
diff_hunk: @@ -819,6 +819,16 @@ def allows_negative_indexes(self):
         """
         return self.base.allows_negative_indexes
 
+    @property
+    def is_slice(self):
+        """
+        Indicates whether the result is a slice of a container.
+
+        Indicates whether the result is a slice of a container or an element
+        of a container.
path: pyccel/ast/variable.py
user: login: EmilyBourne
body: Removed: 0bc878b3f
created_at: 2025-03-29T09:23:28Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: yguclu
body: > Thanks for the explanation! To me it was not clear that by "container" you meant pretty much any object of a user-defined class...

Sorry, I wrote too quickly and left out the important word "also", meaning "in addition to the usual `lists/sets/tuples/dicts`"
created_at: 2025-03-29T13:57:01Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: yguclu
body: My point is simply that it is not common language to refer to any data structure as a container. With "container" one *usually* means an object where you can store pointers to other objects, *often* in arbitrary numbers, and *possibly* at run time.
created_at: 2025-03-29T14:01:17Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: EmilyBourne
body: I have the impression that we aren't quite talking about the same thing. By "multi-level containers" I mean lists of lists, lists of sets, lists of arrays, dicts with lists as key, etc.
All of these are objects where you can store pointers at runtime.
I am not talking about any data structure.
The only case that is not described here but is handled by this PR is a container of user defined classes. In this case I would not describe the class as a container so I would not talk about a multi-level container, however as classes (unlike other scalars) must be stored in pointers, this PR also fixes the errors for those.
created_at: 2025-03-29T15:04:12Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: EmilyBourne
body: Maybe this is clearer:
```suggestion
-   #2258 : Fix missing errors for bad pointer handling in the case of containers with mutable elements.
```
?
created_at: 2025-03-29T15:06:18Z
author_association: MEMBER
diff_hunk: @@ -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 with multi-level containers.
path: CHANGELOG.md
user: login: yguclu
body: Yes, that is much clearer! Thanks
created_at: 2025-03-29T16:42:45Z
author_association: MEMBER
diff_hunk: @@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst.insert(0, d)
     lst.insert(1, e)
-    return lst
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs.py
user: login: yguclu
body: Thanks a lot for going into the details. Clearly you have given a lot of thought to this!

I would like to think more of this, but not within a PR review. Do we have a Pyccel Discussion on this topic?
created_at: 2025-03-29T16:55:43Z
author_association: MEMBER
diff_hunk: @@ -13,4 +13,4 @@ def fn():
     lst = [a, b, c]
     lst.insert(0, d)
     lst.insert(1, e)
-    return lst
+    return len(lst)
path: tests/epyccel/modules/list_user_defined_objs.py
user: login: EmilyBourne
body: No we don't. Several options are discussed on the related issue: #2210.
We also talked about this in a meeting a few weeks ago which is how we landed on reference counting.
created_at: 2025-03-29T17:03:37Z
author_association: MEMBER

PR comments:
user: login: pyccel-bot[bot]
created_at: 2025-03-24T17:17:18Z
body: Hello again! Thank you for this new pull request :star_struck:.

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

author_association: NONE
user: login: EmilyBourne
created_at: 2025-03-24T17:20:24Z
body: Here is your checklist. Please tick items off when you have completed them or determined that they are not necessary for this pull request:
- [x] Write a clear PR description
- [x] Add tests to check your code works as expected
- [x] Update documentation if necessary
- [x] Update Changelog
- [x] Ensure any relevant issues are linked
- [x] Ensure new tests are passing

author_association: MEMBER
user: login: pyccel-bot[bot]
created_at: 2025-03-24T17:39:53Z
body: 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`.

author_association: NONE
user: login: pyccel-bot[bot]
created_at: 2025-03-25T10:41:23Z
body: 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`.

author_association: NONE
user: login: EmilyBourne
created_at: 2025-03-26T07:15:59Z
body: /bot run coverage
author_association: MEMBER
user: login: pyccel-bot[bot]
created_at: 2025-03-26T08:53:35Z
body: Hey @pyccel/pyccel-dev ! @EmilyBourne has just created this great new pull request! Check it out and let me know what you think!

author_association: NONE
user: login: EmilyBourne
created_at: 2025-03-29T17:22:49Z
body: /bot run pr_tests
author_association: MEMBER