Repository: pyccel/pyccel (python)

Issue:
Error when call of function with return of an array
### Describe the bug
Error when call of function with returned of an array because of creating an unknown variable.

### To Reproduce
Python matrix0.py code:
```
import numpy as np

def Mi(x: float, M0: 'float[:, :]') -> 'float[:, :]':
    M1 = x * M0 + 1.    
    return M1

def M_GEN(x: float) -> 'float[:, :, :]':
    M = np.ones((4, 4), dtype = float)
    M2 = np.empty((3, 4, 4), dtype = float)
    for i in range(3):
        M2[i] = Mi(x * i, M)
    return M2
```

pyccel matrix0.py --language=fortran

### Error details
It generates error:

```
ERROR at compilation stage
Traceback (most recent call last):
  File "/home/vadter/Programs/conda3/bin/pyccel", line 8, in <module>
    sys.exit(pyccel())
             ^^^^^^^^
  File "/home/vadter/Programs/conda3/lib/python3.12/site-packages/pyccel/commands/console.py", line 266, in pyccel
    execute_pyccel(filename,
  File "/home/vadter/Programs/conda3/lib/python3.12/site-packages/pyccel/codegen/pipeline.py", line 378, in execute_pyccel
    src_compiler.compile_module(compile_obj=mod_obj,
  File "/home/vadter/Programs/conda3/lib/python3.12/site-packages/pyccel/codegen/compiling/compilers.py", line 355, in compile_module
    self.run_command(cmd, verbose)
  File "/home/vadter/Programs/conda3/lib/python3.12/site-packages/pyccel/codegen/compiling/compilers.py", line 500, in run_command
    raise RuntimeError(err_msg)
RuntimeError: Failed to build module
/home/vadter/Documents/!Docs/mathprog/Python/PYCCEL/8_matrix/__pyccel__/matrix0.f90:41:29:

   41 |       call Mi(x * i, M, M1 = M2(:, :, i))
      |                             1
Error: Actual argument for ‘m1’ must be ALLOCATABLE at (1)
```


Fortran matrix0.f90 code:
```
module matrix0


  use, intrinsic :: ISO_C_Binding, only : f64 => C_DOUBLE , i64 => &
        C_INT64_T
  implicit none

  contains

  !........................................
  subroutine Mi(x, M0, M1) 

    implicit none

    real(f64), allocatable, intent(out) :: M1(:,:)
    real(f64), value :: x
    real(f64), intent(in) :: M0(0:,0:)

    allocate(M1(0:size(M0, 1_i64, i64) - 1_i64, 0:size(M0, 2_i64, i64) - &
          1_i64))
    M1 = x * M0 + 1.0_f64
    return

  end subroutine Mi
  !........................................

  !........................................
  subroutine M_GEN(x, M2) 

    implicit none

    real(f64), allocatable, intent(out) :: M2(:,:,:)
    real(f64), value :: x
    real(f64), allocatable :: M(:,:)
    integer(i64) :: i

    allocate(M(0:3_i64, 0:3_i64))
    M = 1.0_f64
    allocate(M2(0:3_i64, 0:3_i64, 0:2_i64))
    do i = 0_i64, 2_i64
      call Mi(x * i, M, M1 = M2(:, :, i)) ! <------- Unknown M1
    end do
    if (allocated(M)) then
      deallocate(M)
    end if
    return

  end subroutine M_GEN
  !........................................

end module matrix0
```

### Expected behavior
Correct substitution of a variable in a subroutine: "M1 = M2(:, :, i)" -> "M2(:, :, i)".

### Language
Fortran

### Additional context



Pull request description:
Allow saving an array result of a function to a slice
Allow saving an array result of a function to a slice but raise a warning about suboptimal performance. Fixes #2181 

Patch:
diff --git a/.dict_custom.txt b/.dict_custom.txt
index 175add60..8246c1c5 100644
--- a/.dict_custom.txt
+++ b/.dict_custom.txt
@@ -123,3 +123,4 @@ gFTL
 STC's
 gFTL's
 CMake
+suboptimal
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a5d236b..1a104566 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -110,6 +110,7 @@ All notable changes to this project will be documented in this file.
 -   #2001 : Ensure all memory is correctly deallocated in the Python interface in a way that is compatible with all compilers.
 -   #2153 : Fix missing line information when an unknown class method is called.
 -   #2149 : Fix multi-line expressions in `if` conditions.
+-   #2181 : Allow saving an array result of a function to a slice but raise a warning about suboptimal performance.
 -   Lifted the restriction on ndarrays limiting them to rank<15.
 
 ### Changed
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 84963772..ef5e4bc8 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -3452,6 +3452,16 @@ class SemanticParser(BasicParser):
             else:
                 raise NotImplementedError("Cannot assign result of a function without a return")
 
+            if len(results) == 1 and results[0].var.rank > 1 and isinstance(lhs, IndexedElement):
+                temp = self.scope.get_new_name()
+                semantic_temp = self._assign_lhs_variable(temp, d_var, rhs, new_expressions)
+                new_expressions.append(Assign(semantic_temp, rhs))
+                rhs = semantic_temp
+                errors.report((f"Saving the result of the function {func.name} to a slice requires unnecessary "
+                               "data allocation and copies. This has a performance cost. Consider modifying "
+                               f"{func.name} so {lhs} can be passed as an argument whose contents are modified."),
+                        severity='warning', symbol=expr)
+
             # case of elemental function
             # if the input and args of func do not have the same shape,
             # then the lhs must be already declared


Test patch:
diff --git a/tests/pyccel/scripts/return_numpy_arrays.py b/tests/pyccel/scripts/return_numpy_arrays.py
index 830f4c90..7308b1f5 100644
--- a/tests/pyccel/scripts/return_numpy_arrays.py
+++ b/tests/pyccel/scripts/return_numpy_arrays.py
@@ -24,6 +24,20 @@ e = f(1+2j, 3+4j)
 h,g = multi_returns()
 k = single_return() + 1
 
+def Mi(x: float, M0: 'float[:, :]') -> 'float[:, :]':
+    M1 = x * M0 + 1.
+    return M1
+
+def M_GEN(x: float) -> 'float[:, :, :]':
+    M = np.ones((4, 4), dtype = float)
+    M2 = np.empty((3, 4, 4), dtype = float)
+    for i in range(3):
+        M2[i] = Mi(x * i, M)
+    return M2
+
 if __name__ == '__main__':
     print(a, b, c, d, e, h, g, k)
     print(np.array([1,2,3,4]), np.array([1, 3]), np.array([1., 3.]), np.array([False, True]), np.array([1+2j, 3+4j]))
+
+    x = M_GEN(2.0)
+    print(x)
diff --git a/tests/pyccel/test_pyccel.py b/tests/pyccel/test_pyccel.py
index 783bccdc..33965015 100644
--- a/tests/pyccel/test_pyccel.py
+++ b/tests/pyccel/test_pyccel.py
@@ -769,6 +769,7 @@ def test_return_numpy_arrays(language):
     types += [float]*5 # 5 floats for h
     types += [int]*5 # 5 ints for g
     types += [int]*4 # 4 ints for k
+    types += [float]*48 # 48 floats for x
     pyccel_test("scripts/return_numpy_arrays.py", language=language, output_dtype=types)
 
 #------------------------------------------------------------------------------
diff --git a/tests/warnings/semantic/UNNECESSARY_ALLOCATION_IN_FUNCTION.py b/tests/warnings/semantic/UNNECESSARY_ALLOCATION_IN_FUNCTION.py
new file mode 100644
index 00000000..6d904933
--- /dev/null
+++ b/tests/warnings/semantic/UNNECESSARY_ALLOCATION_IN_FUNCTION.py
@@ -0,0 +1,13 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+import numpy as np
+
+def Mi(x: float, M0: 'float[:, :]') -> 'float[:, :]':
+    M1 = x * M0 + 1.
+    return M1
+
+def M_GEN(x: float) -> 'float[:, :, :]':
+    M = np.ones((4, 4), dtype = float)
+    M2 = np.empty((3, 4, 4), dtype = float)
+    for i in range(3):
+        M2[i] = Mi(x * i, M)
+    return M2